dev
Reflection - 02. 어노테이션과 사용자 메타데이터


02. 어노테이션과 사용자 메타데이터

어노테이션은 분석할 선언을 표시한다.

#define CLASS(...)
#define STRUCT(...)
#define ENUM(...)
#define FUNCTION(...)
#define PROPERTY(...)

각 매크로는 바로 뒤의 선언을 분석 대상으로 표시합니다.

STRUCT()
struct Settings
{
	PROPERTY()
	float Speed = { 1.0f };
};

STRUCT() 는 타입을, PROPERTY() 는 멤버를 표시합니다. 표시만 했기 때문에 사용자 메타데이터는 없습니다.

매크로의 대체 내용이 비어 있으므로 일반 컴파일 에서는 다음과 같은 코드가 남습니다.

struct Settings
{
	float Speed = { 1.0f };
};

따라서 사용자 타입은 어노테이션을 붙여도 일반 C++ 타입으로 동작합니다.

생성기는 Clang의 전처리 콜백을 통해 매크로가 확장되는 과정에서 호출 위치와 인자를 수집한다. 따라서 일반 컴파일 결과에서 어노테이션이 제거되더라도, 분석에 필요한 정보를 별도로 보관할 수 있다.

사용자 매크로는 메타데이터 작성법을 제공한다.

#define APP_NAME(value) META("DisplayName", value)
#define APP_UNIT(value) META("Unit", value)

사용자는 키 문자열을 반복하는 대신 이렇게 작성합니다.

PROPERTY(APP_NAME("Speed"), APP_UNIT("m/s"))
float Speed = { 1.0f };

생성기가 인자를 확장해 다음 형태의 토큰으로 만듭니다.

META("DisplayName", "Speed"), META("Unit", "m/s")

PROPERTY는 뒤의 선언을 분석 대상으로 표시하는 어노테이션이다. APP_NAME과 APP_UNIT은 사용자가 정의한 편의 매크로이며, 생성기는 이 이름을 별도로 알 필요가 없다. 인자를 확장한 결과가 META("key", value) 형식이면 메타데이터 파서가 처리한다.

여기서 META는 실행되는 C++ 함수가 아니라, 생성기가 인식하는 토큰 형식이다. 일반 컴파일에서는 바깥의 PROPERTY(...)가 제거되므로 META(...)도 실행 코드에 남지 않는다.

메타데이터 파서는 토큰을 값으로 변환한다.

META("Key", value)

위의 형태를 가지고있는 토큰을 이후에 메타데이터로 저장합니다.

using MetadataValue = 
std::variant<bool, std::int64_t, std::uint64_t, double, std::string>;

struct MetadataEntry
{
    std::string Key;
    MetadataValue Value;
};

이를 통해 앞의 두 예제로부터 두 항목을 얻습니다.

Key: DisplayName    Value: string "Speed"
Key: Unit           Value: string "m/s"

지원하는 값은 일반 문자열, Boolean, 정수, 부동소수점 리터럴 입니다.

아래와 같은 표현은 지원되지 않습니다.

META("RangeMax", 10.0 * 2.0)

숫자 리터럴로 확장되는 매크로는 사용할 수 있습니다.

#define SPEED_LIMIT 20.0
#define APP_MAX(value) META("RangeMax", value)

PROPERTY(APP_MAX(SPEED_LIMIT))
float Speed = 1.0f;

같은 선언에 동일한 키를 두 번 작성하면 파싱 오류가 됩니다.

메타데이터의 의미는 소비자가 결정한다.

리플렉션 모듈은 메타데이터의 키와 값을 저장하고 조회하는 기능을 제공한다.

각 키를 어떤 기능에 사용할지는 모듈을 사용하는 애플리케이션이 결정한다.

PROPERTY(META("DisplayName", "이동 속도"), META("Editable", true), META("Unit", "m/s"))
float Speed = 1.0f;

이 선언에서 모듈은 DisplayName , Editable ,Unit 값을 보관한다.

에디터는 이를 읽어 표시 이름을 변경하거나, 입력창을 활성화하거나, 단위를 표시할 수 있다.

하지만 이러한 동작은 에디터가 직접 구현해야 한다.

특히 Editable = true 가 실제 C++ 멤버의 쓰기 가능 여부를 바꾸지는 않는다.

PROPERTY(META("Editable", true))
const float Speed = 1.0f;

이 경우 메타데이터는 편집을 가능하다는 의도를 나타내지만, 실제 멤버는 const 이므로 Runtime의 쓰기 기능은 지원되지 않는다. 소비자는 메타데이터와 실제 접근 가능 여부를 함께 확인해야 한다.

이처럼 데이터의 보관과 해석 정책을 분리하면 리플렉션 모듈이 특정 에디터나 엔진의 규칙에 의존하지 않는다.

서로 다른 애플리케이션이 같은 정보를 각자 목적에 맞게 활용할 수 있다.