[Unreal Engine] Reflection System - Collection 1

Imeamangryang·2025년 6월 24일

Unreal Reflection System

목록 보기
5/10
post-thumbnail

출처 : staticJPL - Unreal-Engine-Core-Documentation


Collection

생성 섹션에서는 다양한 타입별 코드 스니펫이 삽입되어 컬렉션 및 등록을 위한 준비가 이루어졌습니다. 이번 섹션의 목적은 C++의 정적 자동 등록(Static Automatic Registration)을 통해 타입 정보가 어떻게 수집되는지 자세히 살펴보는 것입니다. Unreal UHT가 생성한 코드는 타입 정보의 메타데이터와 함께 여러 파일 및 DLL 모듈에 분산되어 있습니다. 이 정보를 효율적으로 정리하고 엔진 초기화 시 순차적으로 등록하기 위해 Unreal은 정적 자동 등록 시스템을 사용합니다. 이 시스템은 정적으로 할당된 객체를 등록하고 체계적으로 연결하며, 엔진 구조를 조직하는 데 있어 모듈(Module)이 중요한 역할을 합니다. 각 모듈은 자체 스크립트 구성을 가지며, 상위 레벨의 게임 모듈은 Core Module, CoreUObject와 같은 필수 하위 모듈에 의존합니다.

C++ 표준에서는 서로 다른 컴파일 유닛에 존재하는 전역 정적 변수의 초기화 순서를 명확히 규정하지 않으며, 이는 컴파일러의 구현에 따라 달라질 수 있습니다. 이러한 정적 순서의 불확실성 때문에, 이에 의존하지 않는 방식이 최적의 해결책으로 여겨집니다. 하지만 엔진에서 리플렉션 객체를 등록하기 전에, 각 DLL 모듈별로 정보를 정적으로 수집하는 중요한 단계가 필요합니다. 이 정적 컬렉션 단계는 사전 등록(Pre-registration) 단계로 볼 수 있습니다.


Static Collection

UECodeGen_Private 네임스페이스는 타입 정보를 수집하는 정적 데이터 구조와 등록에 필요한 생성자 등 핵심 헬퍼 함수들을 캡슐화합니다. 이러한 정보 수집은 코드 생성 단계에서 삽입되는 매크로들에 의해 시작됩니다. 아래에 간단한 요약과 추가 맥락을 제공합니다.

IMPLEMENT_CLASS_NO_AUTO_REGISTRATION

이 매크로는 "GetPrivateStaticClass"와 등록 정보를 구현하지만, 실제 클래스 등록은 지연시키면서 등록 정보만 선언합니다.

여기서 중요한 차이점은 Inner Singleton과 Outer Singleton의 개념입니다:

  • Inner Singleton: 실제 타입 정보로 채워진 리플렉션 객체를 보관합니다.
  • Outer Singleton: 리플렉션 객체의 스켈레톤(뼈대) 참조를 보관합니다.

예를 들어 UClass의 경우, 먼저 Inner Singleton이 미리 초기화된 객체(포인터는 null)로 정적으로 설정됩니다. 등록 시점에 StaticClass()가 호출되면 데이터가 채워지고, UClass는 지연 등록 중 Inner Singleton이 먼저 호출되는 특수 케이스입니다. 이는 UClass가 사전 등록 단계에서 UObject로 인스턴스화된다는 점에서 다소 의외일 수 있지만, 그 목적은 이후에 더 명확해집니다. 아래 구조체는 주요 타입 포인터를 보관하며, 여러 곳에서 전달되는 타입입니다.

/**
 * 클래스, 구조체 또는 열거형에 대한 등록 정보를 나타내는 구조체입니다.
 */
template <typename T, typename V>
struct TRegistrationInfo
{
   using TType = T;
   using TVersion = V;

   TType* InnerSingleton = nullptr;
   TType* OuterSingleton = nullptr;
   TVersion ReloadVersionInfo;
};

또한, 경우에 따라 UObject, UStruct, UScript, UModel 등과 같은 기본 네이티브 객체들도 존재할 수 있습니다.

이러한 경우에도 해당 객체들은 리플렉션 대상이 됩니다. 소스 코드를 살펴보면 IMPLEMENT_CORE_INTRINSIC_CLASS 매크로를 확인할 수 있으며, 이는 내부적으로 IMPLEMENT_INTRINSIC_CLASS로 이어집니다. 주목할 점은 이러한 객체들은 별도의 generated.h 또는 generated.cpp 파일이 존재하지 않는다는 것입니다.

#define IMPLEMENT_INTRINSIC_CLASS(TClass, TRequiredAPI, TSuperClass, TSuperRequiredAPI, TPackage, InitCode) \  
    TRequiredAPI UClass* Z_Construct_UClass_##TClass(); \  
    extern FClassRegistrationInfo Z_Registration_Info_UClass_##TClass; \  
    struct Z_Construct_UClass_##TClass##_Statics \  
    { \  
       static UClass* Construct() \  
       { \  
          extern TSuperRequiredAPI UClass* Z_Construct_UClass_##TSuperClass(); \  
          UClass* SuperClass = Z_Construct_UClass_##TSuperClass(); \  
          UClass* Class = TClass::StaticClass(); \  
          UObjectForceRegistration(Class); \  
          check(Class->GetSuperClass() == SuperClass); \  
          InitCode \  
          Class->StaticLink(); \  
          return Class; \  
       } \  
    }; \  
    UClass* Z_Construct_UClass_##TClass() \  
    { \  
       if (!Z_Registration_Info_UClass_##TClass.OuterSingleton) \  
       { \  
          Z_Registration_Info_UClass_##TClass.OuterSingleton = Z_Construct_UClass_##TClass##_Statics::Construct();\  
       } \  
       check(Z_Registration_Info_UClass_##TClass.OuterSingleton->GetClass()); \  
       return Z_Registration_Info_UClass_##TClass.OuterSingleton; \  
    } \
    IMPLEMENT_CLASS(TClass, 0)

여기서 Outer Singleton이 정의됩니다. IMPLEMENT_CLASS(TClass, 0) 매크로는 네이티브 오브젝트의 등록 시 Inner Singleton을 정의합니다.

질문:

"왜 모든 등록 작업을 static 콜백에서 직접 실행하지 않고, 등록을 지연시켜야 할까요?"

Dazhao의 설명에 따르면,

"UE4에는 약 1,500개의 클래스가 있습니다. 만약 static 초기화 단계에서 이 모든 클래스를 수집하고 등록한다면, main 함수가 실행되기 전에 상당한 시간이 소요됩니다. 사용자가 프로그램을 더블 클릭했을 때 바로 반응하지 않고, 창이 뜨기까지 한참 기다려야 하는 현상이 발생합니다. 따라서 static 초기화 콜백에서는 가능한 한 적은 작업만 수행하여 프로그램이 최대한 빠르게 시작되도록 하는 것이 중요합니다. 창이 표시되고 배열 구조에 데이터가 이미 준비된 이후에는, 멀티스레딩이나 지연 실행 등 다양한 최적화 기법을 활용해 프로그램 실행 경험을 크게 개선할 수 있습니다."

아래 각 타입별 섹션에서는 SVG 다이어그램을 통해 타입별 코드 수집 과정을 시각적으로 보여줍니다. 이 다이어그램들은 생성된 코드가 등록되기 전까지 어떻게 전달되는지 한눈에 파악할 수 있도록 도와줍니다.


UClass & UFunction Collection

Static Collection 단계

UClass

  1. UClass 구조체의 Z_Construct 정적 선언이 UHT에 의해 생성되어 삽입됩니다.
  2. UClass의 Dependent Singleton들은 UECodeGen_Private에서 생성에 사용되는 함수 포인터 참조명을 수집합니다.
  3. Class_MetaDataParams는 블루프린트 정보를 위해 수집됩니다.
  4. NewProp_Health는 멤버 프로퍼티의 예시로, 프로퍼티 포인터를 수집합니다.
  5. StaticCPPClassInfo는 클래스가 추상 클래스인지 여부를 정의하며, UClass 플래그에 사용됩니다.
  6. ClassParams는 1~5번의 정보를 모두 수집합니다.
  7. ClassParams는 OuterSingleton과 함께 UECodeGen_Private::ConstructUClass로 전달됩니다.

UFunction

  1. UFunction 구조체의 Z_Construct 정적 선언이 UHT에 의해 생성되어 삽입됩니다.
  2. Implementable, Native, Callable FuncParameters가 각각의 함수 이름 문자열 리터럴을 보유하며, Z_Construct는 해당 UClass를 참조합니다. 또한 Z_Construct 함수의 Statics도 정의됩니다.
  3. Implementable, Native, Callable은 이전에 수집된 함수 포인터 및 함수 파라미터와 함께 UECodeGen_Private::ConstructUFunction으로 전달됩니다.
  4. FuncInfo는 UECodeGen_Private::ConstructUFunction의 결과로 채워집니다.
  5. FuncInfo는 ClassParams 구조체와 함께 전달됩니다.

Static Pre-Registration

UClass

  1. Z_Struct_CompiledInDeferFile이 static FClassRegisterCompiledInfo 타입의 ClassInfo[]로 선언됩니다.
  2. IMPLEMENT_CLASS_NO_AUTO_REGISTRATION 매크로가 템플릿화된 Registration_Info_UClass와 GetPrivateStaticClass()를 선언합니다.
  3. ZConstruct_UClass 함수 포인터와 RegistrationInfo_UClass가 함께 ClassInfo에 전달됩니다.
  4. ClassInfo[]는 FRegisterCompiledInInfo Z_CompiledInDeferFile 내부에서 Registration Phase를 위해 수집되며, 구조체 생성자가 호출될 때 FClassDeferredRegistry로 전달됩니다.

Static Collection Summary

정적 컬렉션의 목적은 타입 정보의 수집에 있으며, 이 단계에서는 로딩 순서가 중요하지 않습니다. UECodeGen_Private 네임스페이스는 OuterSingleton을 설정하고 생성하는 역할을 하며, 메모리에 할당된 후 Inner Singleton의 값을 스켈레톤에 설정합니다. 이 과정은 StaticClass() 호출 시 GetPrivateStaticClassBody를 통해 등록 단계에서 이루어집니다. 많은 함수 포인터가 전달되는 이유는 C++ 생성자를 직접 호출하지 않고, 런타임(예: 블루프린트에서 객체를 생성하거나 액터를 씬에 드래그할 때)에서 리플렉션 객체를 지연 초기화(lazy initialization) 방식으로 생성하기 위함입니다. 함수 포인터를 활용한 이 방식은 타입 객체의 생성 시점을 유연하게 조정할 수 있게 해줍니다. 클래스는 프로퍼티와 함수, 그리고 각 함수의 파라미터 정보를 포함하고 있습니다. 또한, UFunction의 생성이 UClass의 생성보다 먼저 이루어져야 한다는 점도 주목할 필요가 있습니다.

void ConstructUClass(UClass*& OutClass, const FClassParams& Params)  
{  
if (OutClass && (OutClass->ClassFlags & CLASS_Constructed))  
{       
	return;  
}  
for (UObject* (*const *SingletonFunc)() = Params.DependencySingletonFuncArray, *(*const *SingletonFuncEnd)() = SingletonFunc + Params.NumDependencySingletons; SingletonFunc != SingletonFuncEnd; ++SingletonFunc)  
{      
	 (*SingletonFunc)();  
}  
UClass* NewClass = Params.ClassNoRegisterFunc();  
OutClass = NewClass;

Params.ClassNoRegisterFunc();

struct FClassFunctionLinkInfo  
{  
    UFunction* (*CreateFuncPtr)();  
    const char* FuncNameUTF8;  
};

CreateFuncPtr와 Params.ClassNoRegisterFunc() 모두 정의된 UClass 객체와 필요한 UFunction* 을 반환하지만, 왜 이러한 포인터를 직접 사용하지 않는지 궁금할 수 있습니다. 그 이유는 생성 순서의 불확실성 때문입니다. 타입 간의 의존성을 관리하려면 모든 타입이 올바른 순서로 정의되어야 하며, 이를 강제로 빌드 종속성 계층을 만들어 해결할 수도 있지만, 규모가 커질수록 현실적으로 불가능해집니다.

이 문제를 해결하기 위해 싱글턴 패턴과 지연 평가(lazy evaluation)를 함께 사용합니다. 객체가 아직 생성되지 않았다면 생성해서 반환하고, 이미 생성된 경우에는 기존 객체를 반환합니다. Outer Singleton과 Inner Singleton 패턴은 이러한 객체 생성과 의존성 관리를 필요할 때 처리할 수 있도록 하여, 훨씬 유연하고 확장성 있는 구조를 제공합니다.


Uinterface Collection

Static Collection 단계

  1. UInterface 구조체의 Z_Construct 정적 선언이 UHT에 의해 생성되어 삽입됩니다.
  2. UInterface의 Dependent Singleton들은 Z_Construct_UClass_UInterface와 패키지(Owner)에 대한 함수 포인터를 수집합니다.
  3. Class_MetaDataParams는 블루프린트 정보를 위해 수집됩니다.
  4. StaticCPPClassTypeInfo는 클래스 플래그로 Abstract로 설정됩니다.
  5. FuncParams가 생성된 후 FuncInfo 데이터가 수집됩니다.
  6. ClassParams는 1~5번의 데이터를 모두 수집합니다.
  7. ClassParams는 OuterSingleton과 함께 UECodeGen_Private::ConstructUClass로 전달됩니다.

Static Pre-Registration

  1. Z_Struct_CompiledInDeferFile은 static FClassRegisterCompiledInfo 타입의 ClassInfo[]로 선언됩니다.
  2. IMPLEMENT_CLASS_NO_AUTO_REGISTRATION 매크로가 템플릿화된 Registration_Info_UClass와 GetPrivateStaticClass()를 선언합니다.
  3. ZConstruct_UClass 함수 포인터와 RegistrationInfo_UClass가 함께 ClassInfo에 전달됩니다.
  4. ClassInfo[]는 FRegisterCompiledInInfo Z_CompiledInDeferFile 내부에서 Registration Phase를 위해 수집되며, 구조체 생성자가 호출될 때 FClassDeferredRegistry로 전달됩니다.

Uinterface Collection Summary

일반 클래스는 UObject를 상속해야 하며, UInterface는 특별한 클래스이지만 여전히 UClass로 저장됩니다. 수집 및 사전 등록 단계에서 볼 수 있듯이, UInterface 역시 UClass와 동일한 생성 경로를 따릅니다.


UStruct UEnum & Property Collection

UStruct는 일반 C++의 구조체와 유사하게 다양한 타입을 보관하고, 특정 메모리 레이아웃으로 인스턴스를 생성할 수 있는 집합(aggregate) 타입입니다.

이제 코드 흐름을 간단히 살펴보며, Unreal에서 집계 타입이 어떻게 FField로 표현되고, 다시 UnrealScriptType의 공통 래퍼인 FProperty로 변환되는지 알아보겠습니다.

Properties (FField,FVariant,TProperty,FProperty)

Inside UECodeGenPrivate

enum class EPropertyGenFlags : uint8  
{  
    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,  
    //                = 0x08,  
    //                = 0x09,    Float             = 0x0A,  
    Double            = 0x0B,  
    Bool              = 0x0C,  
    SoftClass         = 0x0D,  
    WeakObject        = 0x0E,  
    LazyObject        = 0x0F,  
    SoftObject        = 0x10,  
    Class             = 0x11,  
    Object            = 0x12,  
    Interface         = 0x13,  
    Name              = 0x14,  
    Str               = 0x15,  
    Array             = 0x16,  
    Map               = 0x17,  
    Set               = 0x18,  
    Struct            = 0x19,  
    Delegate          = 0x1A,  
    InlineMulticastDelegate = 0x1B,  
    SparseMulticastDelegate = 0x1C,  
    Text              = 0x1D,  
    Enum              = 0x1E,  
    FieldPath         = 0x1F,  
    LargeWorldCoordinatesReal = 0x20,  
  
    // Property-specific flags  
    NativeBool        = 0x40,  
    ObjectPtr         = 0x40,  
  
};

EPropertyGenFlags는 템플릿 타입 파라미터로 사용됩니다. Unreal Engine의 UObjectGlobals.h 코드에서는 여러 구조체에서 이 플래그가 활용되는 것을 확인할 수 있습니다.

// We don't want to use actual inheritance because we want to construct aggregated compile-time tables of these things.  
struct FPropertyParamsBase  
{  
    const char*    NameUTF8;  
    const char*       RepNotifyFuncUTF8;  
    EPropertyFlags    PropertyFlags;  
    EPropertyGenFlags Flags;  
    EObjectFlags   ObjectFlags;  
    SetterFuncPtr  SetterFunc;  
    GetterFuncPtr  GetterFunc;  
    uint16         ArrayDim;  
};
struct FPropertyParamsBaseWithOffset // : FPropertyParamsBase  
{  
    const char*    NameUTF8;  
    const char*       RepNotifyFuncUTF8;  
    EPropertyFlags    PropertyFlags;  
    EPropertyGenFlags Flags;  
    EObjectFlags   ObjectFlags;  
    SetterFuncPtr  SetterFunc;  
    GetterFuncPtr  GetterFunc;  
    uint16         ArrayDim;  
    uint16         Offset;  
};

더 깊이 살펴보면, FGeneric, FByte, FBool, FObject, FInterface, FStruct 등 다양한 구조체 파라미터(ParamsBase로 명명됨)가 정의되어 있음을 알 수 있습니다. 여기서 주의할 점은, 이 구조체들은 UECodeGen_Private에 정의된 파라미터 구조체들과 동일하지 않다는 것입니다. 실제로 다른 구조체들이 이러한 타입을 보유하고 있으며, 이 타입들이 ConstructFProperties 함수로 전달됩니다.

Starting with ConstructUScriptStruct

void ConstructUScriptStruct(UScriptStruct*& OutStruct, const FStructParams& Params)  
{  UObject*                      (*OuterFunc)()     = Params.OuterFunc;  
   UScriptStruct*                (*SuperFunc)()     = Params.SuperFunc;  
   UScriptStruct::ICppStructOps* (*StructOpsFunc)() = (UScriptStruct::ICppStructOps* (*)())Params.StructOpsFunc;  
   UObject*                      Outer     = OuterFunc     ? OuterFunc() : nullptr; 
   UScriptStruct*                Super     = SuperFunc     ? SuperFunc() : nullptr; 
   UScriptStruct::ICppStructOps* StructOps = StructOpsFunc ? StructOpsFunc() : nullptr;  

   if (OutStruct)  
   {         
		return;  
   }  
   UScriptStruct* NewStruct = new(EC_InternalUseOnlyConstructor, Outer, UTF8_TO_TCHAR(Params.NameUTF8), Params.ObjectFlags) UScriptStruct(FObjectInitializer(), Super, StructOps, (EStructFlags)Params.StructFlags, Params.SizeOf, Params.AlignOf);  
   OutStruct = NewStruct;  

   ConstructFProperties(NewStruct, Params.PropertyArray, Params.NumProperties);  

   NewStruct->StaticLink();  

#if WITH_METADATA  
   AddMetaData(NewStruct, Params.MetaDataArray, Params.NumMetaData);  
#endif  
}

ConstructFProperties 함수 호출 시, 새로 할당된 UScriptStruct가 집계 데이터로 채워지게 됩니다. 이때 Outer는 클래스나 NewFunction 등으로 전달될 수 있습니다. 예시에서 "Score" 멤버 프로퍼티를 정의했는데, 이는 UHT에 의해 다음과 같이 변환됩니다.

static const UECodeGen_Private::FFloatPropertyParams NewProp_Score;

하지만 실제로는 PropPointer[] 배열에 의해 수집됩니다.

const UECodeGen_Private::FPropertyParamsBase* const Z_Construct_UScriptStruct_FMyStruct_Statics::PropPointers[] = {  
   (const UECodeGen_Private::FPropertyParamsBase*)&Z_Construct_UScriptStruct_FMyStruct_Statics::NewProp_Score,  
};

여기서 반환 타입은 FPropertyParamsBase*입니다. 하지만 우리가 수집한 것은 FFloatPropertyParams 타입입니다. 그렇다면 이 둘은 어떻게 호환될까요? 실제로는 다양한 타입이 정의되어 있으며, 아래와 같이 여러 타입이 존재합니다:

// 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 FUInt16PropertyParams;  
typedef FGenericPropertyParams FUInt32PropertyParams;  
typedef FGenericPropertyParams FUInt64PropertyParams;  
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;

FFloatPropertyParams는 실제로 FGenericPropertyParams의 typedef이며, FGenericPropertyParams의 메모리 레이아웃은 다음과 같습니다.

struct FGenericPropertyParams // : FPropertyParamsBaseWithOffset  
{  
   const char*      NameUTF8;  
   const char*       RepNotifyFuncUTF8;  
   EPropertyFlags    PropertyFlags;  
   EPropertyGenFlags Flags;  
   EObjectFlags     ObjectFlags;  
   SetterFuncPtr  SetterFunc;  
   GetterFuncPtr  GetterFunc;  
   uint16           ArrayDim;  
   uint16           Offset;  
#if WITH_METADATA  
   uint16                              NumMetaData;  
   const FMetaDataPairParam*           MetaDataArray;  
#endif  
};

이제 흥미로운 부분입니다. 첫 번째로 알 수 있는 사실은 FGenericPropertyParamsFPropertyParamsBaseuint16 Offset까지 메모리 레이아웃이 동일하다는 점입니다!

소스 코드를 분석해보면, 이는 FField 객체를 FProperty 생성 시스템에서 사용할 수 있도록 해주기 위한 것으로 보입니다. 이를 통해 메모리 최적화와 템플릿 기반 다형성을 통한 더 세밀한 제어가 가능해집니다. FStructParams 배열을 더 살펴보면, FPropertyParamsBase*가 모든 필요한 PropertyTypes를 순회하며 생성에 사용되는 것을 알 수 있습니다.

void ConstructFProperties(UObject* Outer, const FPropertyParamsBase* const* PropertyArray, int32 NumProperties)  
{  
   // 포인터를 배열 끝으로 이동한 뒤, 역순으로 프로퍼티를 순회합니다  
   PropertyArray += NumProperties;  
   while (NumProperties)  
   {      
      ConstructFProperty(Outer, PropertyArray, NumProperties);  
   }
}

ConstructFProperty는 UHT가 삽입한 플래그와 EPropertyGenFlags를 활용해, 모든 프로퍼티에 대해 올바른 템플릿 정보를 적용하여 재귀적으로 호출됩니다.

void ConstructFProperty(FFieldVariant Outer, const FPropertyParamsBase* const*& PropertyArray, int32& NumProperties)  
{  
const FPropertyParamsBase* PropBase = *--PropertyArray;  

uint32 ReadMore = 0;  

FProperty* NewProp = nullptr;  
switch (PropBase->Flags & PropertyTypeMask)  
{       default:  
   {  
	  // Unsupported property type  
	  check(false);  
   }  
   case EPropertyGenFlags::Byte:  
   {  
	  NewProp = NewFProperty<FByteProperty, FBytePropertyParams>(Outer, *PropBase);  
   }       break;  

   case EPropertyGenFlags::Int8:  
   {  
	  NewProp = NewFProperty<FInt8Property, FInt8PropertyParams>(Outer, *PropBase);  
   }       break;  

   case EPropertyGenFlags::Int16:  
   {  
	  NewProp = NewFProperty<FInt16Property, FInt16PropertyParams>(Outer, *PropBase);  
   }       break;  

   case EPropertyGenFlags::Int:  
   {  
	  NewProp = NewFProperty<FIntProperty, FIntPropertyParams>(Outer, *PropBase);  
   }       break;  

 ... // Lots of other Case statements code here from engine source.

   case EPropertyGenFlags::Enum:  
   {  
	  NewProp = NewFProperty<FEnumProperty, FEnumPropertyParams>(Outer, *PropBase);  

	  // Next property is the underlying integer property  
	  ReadMore = 1;  
   }       break;  
  ... // Code here 
}  
--NumProperties;  

for (; ReadMore; --ReadMore)  
{      
	ConstructFProperty(NewProp, PropertyArray, NumProperties);  
}}

여기서 리플렉션을 위한 원자 타입 체인이 실제로 생성됩니다. 이 중 일부는 C++ 네이티브 타입과 비교되기도 합니다. 주목할 점은, UHT에서 전달된 플래그에 따라 Prop이 최종적으로 실제 파라미터 타입으로 캐스팅된다는 것입니다. 또한 OuterFField 또는 UObject 타입이 될 수 있는데, FFieldVariant는 생성자 내부에서 이 둘 중 하나로 인스턴스를 초기화할 수 있도록 래퍼 클래스로 사용됩니다.

/**  
 * Special container that can hold either UObject or FField. * Exposes common interface of FFields and UObjects for easier transition from UProperties to FProperties. * DO NOT ABUSE. IDEALLY THIS SHOULD ONLY BE FFIELD INTERNAL STRUCTURE FOR HOLDING A POINTER TO THE OWNER OF AN FFIELD. */

class FFieldVariant  
{  
    union FFieldObjectUnion  
    {  
       FField* Field;  
       UObject* Object;  
    } Container;  
  
    static constexpr uintptr_t UObjectMask = 0x1;  
  
public:  
  
    FFieldVariant()  
    {       
	    Container.Field = nullptr;  
    }  
    
    FFieldVariant(const FField* InField)  
    {
	    Container.Field = const_cast<FField*>(InField);
	    check(!IsUObject());  
    }
    ...
template <typename PropertyType, typename PropertyParamsType>  
PropertyType* NewFProperty(FFieldVariant Outer, const FPropertyParamsBase& PropBase)  
{       
  const PropertyParamsType& Prop = (const PropertyParamsType&)PropBase;  
  PropertyType* NewProp = nullptr;
	
   if (Prop.SetterFunc || Prop.GetterFunc)  
   { 
	   NewProp = new TPropertyWithSetterAndGetter<PropertyType>(Outer, Prop);  
   }
   else  
   {  
	  NewProp = new PropertyType(Outer, Prop);  
   }  
   // Meta data is here in actual source
   return NewProp;  
}

첫 번째 템플릿 파라미터인 PropertyType은 UnrealType.h에 정의된 실제 네이티브 타입을 참조합니다. 두 번째 템플릿 파라미터는 생성된 코드에서 수집된 실제 타입으로 메모리 레이아웃을 다시 맞추는 데 사용됩니다. Reflection Test 프로젝트의 UStruct에서 Score 값은 부동 소수점 타입으로 정의되어 있습니다.

case EPropertyGenFlags::Float:  
{  
    NewProp = NewFProperty<FFloatProperty, FFloatPropertyParams>(Outer, *PropBase);  
}

FFloatProperty 클래스를 살펴보면, 이 클래스에는 DECLARE_FIELD 매크로가 포함되어 있습니다.

#define DECLARE_FIELD_API(TClass, TSuperClass, TStaticFlags, TRequiredAPI) \  
private: \  
    TClass& operator=(TClass&&);   \  
    TClass& operator=(const TClass&);   \  
public: \  
    typedef TSuperClass Super;\  
    typedef TClass ThisClass;\  
    TClass(EInternal InInernal, FFieldClass* InClass) \  
       : Super(EC_InternalUseOnlyConstructor, InClass) \  
    { \  
    } \  
    static TRequiredAPI FFieldClass* StaticClass(); \  
    static FField* Construct(const FFieldVariant& InOwner, const FName& InName, EObjectFlags InObjectFlags); \  
    inline static constexpr uint64 StaticClassCastFlagsPrivate() \  
    { \  
       return uint64(TStaticFlags); \  
    } \  
    inline static constexpr uint64 StaticClassCastFlags() \  
    { \  
       return uint64(TStaticFlags) | Super::StaticClassCastFlags(); \  
    } \  
    inline void* operator new(const size_t InSize, void* InMem) \  
    { \  
       return InMem; \  
    } \  
    inline void* operator new(const size_t InSize) \  
    { \  
       DECLARE_FIELD_NEW_IMPLEMENTATION(TClass) \  
    } \  
    inline void operator delete(void* InMem) noexcept \  
    { \  
       FMemory::Free(InMem); \  
    } \  
    friend FArchive &operator<<( FArchive& Ar, ThisClass*& Res ) \  
    { \  
       return Ar << (FField*&)Res; \  
    } \  
    friend void operator<<(FStructuredArchive::FSlot InSlot, ThisClass*& Res) \  
    { \  
       InSlot << (FField*&)Res; \  
    }  
  
#if !CHECK_PUREVIRTUALS  
    #define IMPLEMENT_FIELD_CONSTRUCT_IMPLEMENTATION(TClass) \  
       FField* Instance = new TClass(InOwner, InName, InFlags); \  
       return Instance; #else  
    #define IMPLEMENT_FIELD_CONSTRUCT_IMPLEMENTATION(TClass) \  
       return nullptr;  
#endif

이 부분이 DECLARE_CLASS 매크로와 매우 유사하게 보이는 것은 당연합니다. FField는 UObject의 구조를 그대로 반영한 데이터 객체용 미러 이미지입니다. UClass에서 GetPrivateStaticClass()를 호출하는 것처럼, FField 역시 자체적으로 CPP 파일에 정의된 IMPLEMENT_FIELD를 통해 동일한 역할을 수행합니다.

#define IMPLEMENT_FIELD(TClass) \  
FField* TClass::Construct(const FFieldVariant& InOwner, const FName& InName, EObjectFlags InFlags) \  
{ \  
    IMPLEMENT_FIELD_CONSTRUCT_IMPLEMENTATION(TClass) \  
} \  
FFieldClass* TClass::StaticClass() \  
{ \  
    static FFieldClass StaticFieldClass(TEXT(#TClass), TClass::StaticClassCastFlagsPrivate(), TClass::StaticClassCastFlags(), TClass::Super::StaticClass(), &TClass::Construct); \  
    return &StaticFieldClass; \  
} \

FFloatProperty의 생성 과정은 템플릿 기반의 깊은 상속 체인을 따라 슈퍼 클래스 생성자가 연쇄적으로 호출되는 구조로 이루어집니다.

상속의 중간 계층은 다소 혼란스러울 수 있습니다. TProperty_Numeric 계층은 네이티브 C++ 타입을 변환하여 구조체 컨테이너에 설정하는 역할을 합니다. 그 다음 계층인 TProperty_WithEqualityAndSerializer는 내부 직렬화 헬퍼 함수들을 제공합니다. 마지막으로, 템플릿화된 TPropertyFProperty 생성자를 호출하게 됩니다.

FProperty::FProperty(FFieldVariant InOwner, const UECodeGen_Private::FPropertyParamsBaseWithOffset& Prop, EPropertyFlags AdditionalPropertyFlags /*= CPF_None*/)  
    : FField(InOwner, UTF8_TO_TCHAR(Prop.NameUTF8), Prop.ObjectFlags)  
    , ArrayDim(1)  
    , ElementSize(0)  
    , PropertyFlags(Prop.PropertyFlags | AdditionalPropertyFlags)  
    , RepIndex(0)  
    , BlueprintReplicationCondition(COND_None)  
    , Offset_Internal(0)  
    , PropertyLinkNext(nullptr)  
    , NextRef(nullptr)  
    , DestructorLinkNext(nullptr)  
    , PostConstructLinkNext(nullptr)  
{  
    this->Offset_Internal = Prop.Offset;  
  
    Init();  
}
profile
언리얼 엔진 주니어 개발자 입니다.

0개의 댓글