본문 바로가기

개발 Note/it 이야기

[C++ 리플렉션] MFC의 CRuntimeClass

반응형

제가 하는 업무가 Platform 이나 Framework을 개발하는 것이기 때문인지 , 어떤 기능이 있었으면 좋겠다는 아이디어가 떠오르거나 막히는 문제가 발생하면, 음, "Windows OS에서는 어떻게 처리했지?" 라는 질문을 던지고 답을 찾아보게 됩니다.

최근에 Open Platform으로 갖춰야 할것 중 하나가 개발 툴인데,
IDE같은 개발 툴들은 대부분 Import, Plug-in, User Defined Control(ActiveX control) 등과 같이, 툴이 만들어지고 나서 새로운 class 나 객체들을 자연스럽게 설치, 추가하여 사용할 수 있도록 해주는 부분들이필요 하게 됩니다.

이런  framework  구현부는 실제 class의 실체를 모른 상태에서 runtime시에 binding 되는 class의 객체를 생성하는 기술이 필요하게 됩니다.

 

class의 인스턴스 생성방법은 다들 알다시피 new를 통해 생성합니다.

BaseClass * pInstance = new SubClass;  

 

바꾸어 말하면 우리는 반드시 SubClass를 알고 있어야 한다는 것입니다. 그래야만 instance를 생성 할 수 있습니다.

아래와 같이 CameraPlugIn class를 framework에서 모르는 상태라고 한다면 절대 instance를 만들 수 없다는 예기죠.

IPlugIn * pPlugIn = new CameraPlugIn;  

만약 우리가  CameraPlugIn를 모르는 상태 즉,  PlugIn Framework 개발당시에는 CameraPlugIn이라는 class를 모르는 상태에서  camera.dll또는 lib을 download받아서, 이를 link 또는 load 해서 사용할 수 있는 기능을 제공하려 할때,
class를 모르는 상태에서 class 를 runtime시에 동적으로 이름이 무엇인지, 알아내서 instance를 생성할 수 있도록 하는 기술이 필요하게됩니다.

이러한 기술을 리플렉션( Reflection) 이라한다.

 

C# 에서도 이와같은 리플렉션은 지원하고 있고, Runtime중에 type binding을 지원하는 Dynaimc 이라는 새로운 타입이 추가될 예정이라고 합니다.

 

그외  루비, 스몰토크, PHP, 파이썬, 펄 등의 언어에서는 리플렉션 객체를 제공하며, 대부분 유사한 형태를 띄고 있습니다.

 


 

아쉽게도 C++ 에서는 공식적으로 지원하지 않는 기능이지만, 전세계의 수많은 똑똑한 개발자 분들이 이런 기능을 다양한 방법으로 제공하고 있습니다.

 

MFC의 CRuntimeClass는 리플랙션과 비슷한 기능을 위해 만들어진 가벼운 Macro 정도이고 실제 훨씬 파워풀하고 플랙서블한 그런 library들도 많이 있으니 관심이 있다면 구글링으로 찾아보셔도 좋을 것입니다.

 


 

그중 하나의 기술 (?, 그렇죠 말그대로 Programming 테크닉이라 불리울 만한 기술이죠. )MFC에서 Runtime class 라는 것이 있습니다.

 


저도 나름 이와 비슷한 아이디어를 떠올리면서 구현을 했었는데 , 거의 구현이 완료되고 나서 생각해보니 MFC에 이런게 있었다는 것이 생각이나, Source code를 뒤져봤답니다.

역시나 이미 상용화되어서 아주 ~~ 아주 잘 쓰고 있는 메크로들이라 그런지, 제가 생각했던 개념과 유사한 뼈대위에 많은 살들이 붙어있더군요.

회사에서 사용하기 위해 작성한 코드가 좋은 예가 될수 있을텐데, 회사의 자산이라 공개를 할 수 없어서  MFC의 Runtime class의 내용을 살펴보는 것으로 대신 하겠습니다.

저는 사실 한 일주일 전쯤 해서  이런 고민에 빠져 있었습니다.

만약 내가 개발 툴을 만들어주고, application 개발자는 자기가 짠 class를 이 개발툴에 plug-in 한 후에 ,
개발 툴에서 재공하는 Generator 를 이용해  Generator("TestClass") 라고 하면 testclass의 instance를 만들어준다고 합시다.
이때 개발툴을 개발할 당시에는 TestClass를 모르고 있기 때문에 일반적인 개발 방법으론, 이를 구현할 수가 없습니다.
즉, Runtime 중에 class를 register하고 이를 parsing하여 instance를 생성 시킬 수 있는 기술이 필요하다.!!

class라는 게 어찌 보면 type인데, 내가 type을 모른 상태에서 object를 생성할 수 있을까?
예를 들면
Object* pA = CreateInstance("TestClass");
라고하면 pA는 class TestClass 로 생성이되는 이런걸 만들고 싶었습니다.
하지만 흔히 이렇게 사용할 수 있으려면, 기본적으로 TestClass를 CreateInstance 내에서 알고 있어야 합니다. 
그래야 "TestClass" 라는 스트링을 parsing한 다음에 TestClass로 생성을 해줄 수가 있기 때문입니다.
그래서 대부분 아래 처럼 머리속에서 코딩을 하겠죠?

 

Object* CreateInstance(char* pClassName) {        
	:
    if(strcmp(pClassName,"TestClass")==0)
    	return (Object*)new TestClass; 
}

CreateInstance를 제너럴하게 만들고 싶어서 pClassName 으로 들어오는 class이름이 무엇이든간에 이에 맞는 class를 찾아서 있으면 해당 class의 instance를 생성해주고, 없으면 null 을 return하기를 원한다면? 어떻게 만들까? 음... class들을 관리하는 table과 class name,그리고 class generator을 만들어서 구현 할 수 있을것 입니다.

Object* CreateInstance(char* pClassName) {
	:        
    for(;;){
    	if(strcmp(pClassName,gClassTable[i].szName)==0)
        	return (Object*) gClassTable[i].Construct(); 
    }
 }

그런데, CreateInstance라는 함수를 만들때는 "TestClass라는 것이 없는데 , 앞으로 누군가가 만들것이다." 라고 한다면? 흠 흠.. 이경우는 난감한가요? 분명 이렇게 되어야 합니다. gClassTable 에 RegisterClass라는 API를 만들어서 이름과 생성자를 등록하여 사용할 수 있도록 해야합니다.

void RegisterClass(char* classname, Object *(*fpConstruct)()) {
	:     
    gClassTable[i].szName = classname;
    gClassTable[i].Construct= fpConstruct; 
 }

헌데 이렇게 되면 상당히 귀찮아지는 것이 fpConstruct를 만드는 일이 됩니다. 개발자의 손이 덜가고 오류를 줄이려면 형식을 제한해야 하는데요.

이를 구현하기 위해서 MFC에서는 runtime class라는 개념이 만들어지게 됩니다. MFC의 CRuntimeClass를 살펴보면, 클레스 전체를 살펴보면 좋겠지만.. 대충 필요한 것들.. 핵심이 될만한 것들만 언급하자면,

struct CRuntimeClass {
// Attributes 
    1) LPCSTR m_lpszClassName; 
    2) CRuntimeClass* m_pBaseClass; 
    3) CObject* CreateObject(); 
    4) static CObject* PASCAL CreateObject(LPCWSTR lpszClassName); 
    5) CRuntimeClass* m_pNextClass;       // linked list of registered classes 
}

아마 가장 핵심이 되는 것이 위의 1)~4) 까지의 내용일 겁니다. 왜냐? 다음 RuntimeClass를 만드는 메크로들을 보면 알 수 있습니다.

#define RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name)) #define DECLARE_DYNAMIC(class_name) \
public: \
    static const CRuntimeClass class##class_name; \
    virtual CRuntimeClass* GetRuntimeClass() const; \
    #define _DECLARE_DYNAMIC(class_name) \
public: \
    static CRuntimeClass class##class_name; \
    virtual CRuntimeClass* GetRuntimeClass() const; \
    #define IMPLEMENT_RUNTIMECLASS(class_name) \
    const CRuntimeClass class_name::class##class_name = { \ 
    	#class_name, sizeof(class class_name), \
	}; \
CRuntimeClass* class_name::GetRuntimeClass() const \
	{ return RUNTIME_CLASS(class_name); } \

이런 메크로들이 있습니다. 약간 길죠?. IMPLEMENT_RUNTIMECLASS 는 설명하기 귀찮은 것들은 다 뺐습니다. 이렇게 메크로만 보면 "뭐하자는 거지?" 라고 의문이 드는 것들이 실제 사용하는 예를 보면 "아하!!" 라고 하게 됩니다.

//in MyTestControl.hpp
//---------------  
class MyTestControl {
  _DECLARE_DYNAMIC(MyTestControl)   //<-- static const Runtimeclass  classMyTestControl 가
                                    // MyTestControl class안에 정의된다.     
        :    
        :
};

 

//in MyTestControl.cpp
//---------------  
//파일 맨 위에 
IMPLEMENT_RUNTIMECLASS(class_name) 
// static 객체인 MyTestControl::classMyTestControl 를 초기화 한다.

//in MyTestApp.cpp  
void main(void) {    
  AfxInitClass(RUNTIME_CLASS(MyTestControl));
  CRuntimeClass::CreateObject("MyTestControl");
}

대충의 설명을 하자면 ,
1. Runtime class로 선언하고자 하는 class 내부에

DECLARE_DYNAMIC(classname)

을 해주고나면, static runtime class 하나가 생기는데

"class"+classname

의 형태로 된다.
2. 이 "classMyTestControl"라고 새로 만들어진 static runtimeclass 는 멤버함수인

3).의 CreateObject()

를 MyTestControl를 생성할 수 있는 함수로 만든다.
3. 그 형식은 아래와 같다.

CreateObject()  {   return new MyTestControl; } 

따라서 우리는 메크로에 의해 생성된 classMyTestControl 이라는 runtime class를 이용해서 MyTestControl이라는 class의 instance를 생성할 수 있게 됩니다.

 

 마지막으로 한가지 설명 안하고 넘어간 것이 있는데.

5) CRuntimeClass* m_pNextClass;       // linked list of registered classes 

이 녀석에 대한 내용이다. 이게 왜 있는가? 제가 개발할때, 위와 같은 구현을 완료하고 나서 Register 단계( MFC에서는 AfxInitClass 단계일듯하다.)에서 결국 table을 만들고, table과 Runtime class를 묶는 작업을 하고, search하는 함수를 만들고... 등등을 작업했었다.

CSystemClassFactroy {
    list runtimeClass;  
    RegisterClass(CRuntimeClass);
};  
application에서 사용할때는 아래와 같이 register를 해야 되는 상황이 있었습니다. 

main() {    
  // application 초기화    
  Factroy.RegisterClass(MyTestControl);  
  :
  :
}

이때 문득 MFC에서는 어떻게 하지? 라는 생각이 들어 이 RuntimeClass를 살펴본바... m_pNextClass 를 이용해서 각 register시에 RuntimeClass들을 줄줄이 사탕으로 엮어놓은 것이라 추측 할수 있었다.!!! 아하.!! 요렇게 하면 runtime class를 managing 하는 manager class가 필요 없겠구나.!! 하는 필(feel)을 받고, 제 코드도 요렇게 바꿨답니다. ㅎㅎ 

 
------------------------------------------------------------------------------------------------------------------------
 

 

잘 동작 않는다는 의견이 있어서 한번 짤막한 코드를 작성해봤습니다.
[셈플 코드]
rt_class.h
#ifndef __RT_CLASS_H__
#define __RT_CLASS_H__

#include <string>
#include <functional>
class Object
{
public:
 Object() = default;
 virtual ~Object() = default;
 virtual void test(){};
};


class RtClass {
public:
 explicit RtClass(std::string name, std::function<Object*(void)> func);
 virtual ~RtClass();
 std::string className;
 std::function<Object*(void)> createObject;
 RtClass* nextClass{nullptr};

 static Object* CreateObject(std::string className);
};


#define RT_CLASS(class_name) ((RtClass*)(&class_name::class##class_name)) 

#define DECL_RT_CLASS(class_name) \
 public: \
 static const RtClass class##class_name; \
 virtual RtClass* GetRuntimeClass() const;

#define IMPL_RT_CLASS(class_name) \
 const RtClass class_name::class##class_name(#class_name, [](void)->Object*{return (Object*)new(std::nothrow) class_name; }); \
 RtClass* class_name::GetRuntimeClass() const \
 { return RT_CLASS(class_name); }


#endif //__RT_CLASS_H__
 
 
 
 
rt_class.cpp

  

 
#include "rt_class.h"
 
static RtClass* pHead = nullptr;
 
Object* 
RtClass::CreateObject(std::string className)
{
  for (auto it = pHead; it != nullptr; it = it->nextClass){
    if (it->className.compare(className) == 0){
     return it->createObject();
    }
  }
 return nullptr;
}
 
RtClass::RtClass(std::string name, std::function<Object*(void)> func)
: className(name)
, createObject(func) {

  if (pHead == nullptr){
    pHead = this;
  }
  else{
    this->nextClass = pHead;
    pHead = this;
  }
}
 
RtClass::~RtClass()
{
 if (this == pHead){
   pHead = pHead->nextClass;
 }
 else{
   for (auto prev = pHead, it = pHead->nextClass; it != nullptr; it = it->nextClass){
     if (it == this){
       prev->nextClass = this->nextClass;
       return;
     }
     prev = it;
   }
 }
}

 

 
 
rt_sample.cpp
 
#include <stdio.h>
#include "rt_class.h"
 
 
class A: public Object{
 DECL_RT_CLASS(A);
 
 virtual void test(){
 printf("I'm A\n");
 };
};
 
 
 
class B: public Object{
 DECL_RT_CLASS(B);
 
 virtual void test(){
 printf("I'm B\n");
 };
};
 
 
IMPL_RT_CLASS(A)
IMPL_RT_CLASS(B)
 
 
 
 
test_main.cpp

 

#include "rt_class.h"


int main(){
 Object* pA = RtClass::CreateObject("A");

 pA->test();


 Object* pB = RtClass::CreateObject("B");
 pB->test();
 return 0;
}

 

 

구조를 잠깐 설명하자면,
1. rt_class 는 platform에 해당 합니다.
2. rt_sample.cpp 는 개발자 본인이나 다른 개발자가 개발하여 platform에 등록(추가) 되는 모듈입니다. ( 이부분이 update가 되는 부분)
  ( lib이나 dll 등으로 가져와서 사용한다고 생각하면 됩니다.)
3. test_main.cpp 는 어플리케이션입니다. 
 
이런 의도로 작성되었고, 실제로 rt_class.so, rt_sample.so, test_main 으로 나눠서 작업해봐도 동작할것입니다.
 
해당 application은 A와 B가 어딘가에서 제공된다는것을 알고 있지만 class는 모릅니다.
"A" 와 "B" 라는 이름(string)으로 클래스를 생성 하여 테스트 하는 코드입니다.
 
test_main은 실제 A와 B를 모르기 때문에, Object라는 상위 class가 제공하는 기능밖에 사용 할 수 없습니다.
test()라는 함수죠.!
그래서 이 방식으로 리플렉션(refection)을 완전히 대처하기는 힘듭니다.
 
Object에 공통된 interface들을 만들고 사용하는데에는 용이합니다. ( 큰 오버해드 없이 간략하게 사용할 수 있는 구조입니다.)
 
 
결과는 : 
I'm A
I'm B
 
 
찾아보기 키워드!!! 

[SEAL Reflex] 
[LibReflection] 
[XCppRefl 라이브러리]

 
<참고 링크>
[MSDN : C++ 리플렉션 in Visual C++ ]http://msdn.microsoft.com/ko-kr/library/y0114hz2(VS.90).aspx