문득 궁금해졌다
- 회사에서 일을 하다가 문득 “언리얼은 어떻게 생성자를 호출해주지?”가 궁금해졌다.
- 생각해보자. 언리얼은 C++을 쓰고 C++은 기본적으로 리플렉션을 지원하지 않는다.
- 그 말인 즉슨 내가 만든
class ATempCharacter : public ACharacter
라는 클래스는 실제 C++ 코드로 new ATempCharacter()
를 명시해주지 않는 한, 내가 만든 ATempCharacter
라는 클래스를 인스턴스화 하고 생성자를 호출할 수 없는 것이다.
- 아무리 언리얼이 UHT를 통해 리플렉션을 지원한다 한들, C++은 언어 자체가 생성자를 호출하려면 해당 객체를 무조건 명시적으로 코드를 통해 생성해줘야 하도록 돼있다. 게다가 C++은 생성자의 함수 포인터를 얻어올 수 있는 방법도 지원하지 않기 때문에 이는 더욱이 불가능하다.
- 그런데, 언리얼은 이걸 해내고 있다. 그 증거로 우리는 직접 코드에
new ATempCharacter()
를 명시해주지 않아도 블루프린트 클래스를 World에 드래그 앤 드랍만 해도 해당하는 하위 객체를 스폰할 수 있다.
- 과연 어떻게 하는 것일까? 나는 너무 궁금해 언리얼
ACharacter
클래스의 생성자에 중단점을 걸어두고 콜스택을 추적해봤다.
충격적인 결과…
- 물론 아래 보여줄 코드는 언리얼 엔진의 코드와는 완전히 동일하지 않다. 언리얼은 기본적으로 템플릿과 매크로로 떡칠이 돼있기에 이해하기 쉽지않아 이를 풀어 쓴 코드라고 생각하면 된다.
- 그러나 기본적인 원리는 아래와 같은 구조로 돼있다.
#include<stdio.h>
#include<unordered_map>
class TestClass {
public:
void __DefaultConstruct(const char* InStr)
{
new(this) TestClass(InStr);
}
TestClass(const char* InStr) {
printf("TestClass: %s\n", InStr);
}
void Print(const char* InStr)
{
printf("%s\n", InStr);
}
};
int main()
{
typedef void(TestClass::* ConstructorPtr) (const char* InStr);
ConstructorPtr funcPtr = &TestClass::__DefaultConstruct;
TestClass* Data = (TestClass*)malloc(sizeof(TestClass));
((Data)->*funcPtr)("asdf");
return 0;
}
- 위와 같은 방식으로 생성자의 함수 포인터와 클래스 사이즈를 UHT를 통해
generated.h
에 구워서 UClass*
에 데이터로 담아주면 인스턴스의 생성을 코드에서 명시적으로 해주지 않아도 똑같은 결과를 내줄 수 있다.
솔직히…
- 저 코드를 찾아보면서 정말 이마를 탁 쳤다. 저런식으로 창의적인 코드를 짤 수 있다니…
- 사실 좋은 방향으로 창의적이라는 뜻은 아닙니다… 개인적으론 좀 거시기한 코드라고 생각해요… 하하하