블로그 이미지
대갈장군

calendar

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31

Notice

제목이 무척 도발적이다. C++/CLI 10분만에 배우기... 글 읽는데만 10분 넘게 걸리는데 어떻게 10분만에 배운다는 거냐...

참고로 출처는 http://www.codeproject.com/Articles/19354/Quick-C-CLI-Learn-C-CLI-in-less-than-10-minutes (저자: Elias Bachaalany)

기본적으로 이 글의 취지는 C++/CLI를 빠르게 시작하기 위한 글이다. 고로 독자들이 C++이랑 .NET 배경지식이 어느정도 있다고 가정하고 글을 쓴다고 한다. 글을 일기전에 자고있는 당신의 머리 세포들을 깨워줄 좋은 글도 있다고 추천해주는 저자의 센스! 


아래에 포함되는 예제를 실행하려면 다음과 같이 하면 된다.

"C:\Program Files\Microsoft Visual Studio 8\Common7\Tools\vsvars32.bat"

요로코롬 하면 저 명령 이후로는 Visual Studio 2005 x86을 이용하여 컴파일을 하게 된다.


그리고 어떤 파일을 컴파일 하고 싶다면 다음과 같이...

cl your_file.cpp /clr

뒤에 붙은 /clr 스위치가 정겨워 보인다...


So, What is C++/CLI?

이것에 대한 대답은 이미 다른 글에서 많이 했는데 일단 이 글을 쓴 저자의 말을 옮겨 보면, C++은 알다시피 C의 Superset (상위셋)이라고 할 수있다. C++은 C에 OOP (객체 지향성) 및 Template를 추가한 것이다. 그렇다면 CLI는 무엇인가?


CLI은 Common Language Infrastructure의 줄임말로 그 뜻은 다음과 같다.

It is an open specification that describes the executable code and runtime environment that allows multiple high-level languages to be used on different computer platforms without being rewritten for specific architectures.

CLI라 함은 실행 코드와 런타임 환경을 정의하는 오픈된 명세로써 많은 고레벨 언어가 운영체제가 다른 플랫폼에서 코드의 재작성 없이 사용될 수 있도록 해주는 것이란다.


Handles and Pointers

아마도 한번이라도 C++/CLI를 써 본 사람이라면 '^' 요 마크가 친숙 할 것이다. 변수 선언시에 꼭 따라 붙는 놈이다. 알다시피 C++에서는 포인터를 선언할 때 * 마크를 사용한다. C++/CLI에서는 ^를 이용해 핸들을 선언한다. 자, "*" 는 Native Pointer를 말하고 이것은 CRT 힙에 상주한다. 반면 "^"는 핸들을 선언하는 것이고 "Safe Pointer"라 불리며 Managed 힙 공간에 상주한다. 이 핸들이라고 하는 녀석은 단순히 생각해서 C++의 레퍼런스와 같은 개념이긴 하지만 C++과는 다른게 (Native 와는 다른게) 제대로 해제하지 않더라도 메모리 리크 (유출)이 발생하지 않는다. 왜냐면 가비지 컬렉터 (GC)가 알아서 메모리를 수거해주기 때문이다. 오우 나이스!


특정 클래스나 값 타입의 레퍼런스를 만들기 위해서는 gcnew라는 키워드를 사용해서 선언해야 한다. 다음과 같이..

System::Object ^x = gcnew System::Object();

nullptr이 흔히 사용하는 null의 CLI 버전이고 ^ 심볼과 함께 % 심볼도 사용된다. 


N* pn = new N; // allocate on native heap
N& rn = *pn; // bind ordinary reference to native object
R^ hr = gcnew R; // allocate on CLI heap
R% rr = *hr; // bind tracking reference to gc-lvalue

C++에서 *과 &이 한 묶음 이듯이 CLI에서는 ^이랑 %가 한 묶음이다. 


Let us get started: Hello World

자 이제 가장 단순한 프로그램 하나 만들어보자. 

#using <mscorlib.dll>

using namespace System;

int main(array<System::String ^> ^args)
{
  System::Console::WriteLine("Hello world");
  return 0;
}

다른 건 뭐 다 봐줄만 한데 main의 입력 함수로 들어오는 놈이 예사롭지 않아보인다. 하지만 C++ 코드를 떠올려보면 저것이 의미하는 것이 그렇게 복잡한 것만은 아니다. 


Classes and UDTs

Classes

클래스를 어떻게 선언하는지 일단 살펴보자. 해줘야 할 것은 단 한가지 뿐. Protection Modifier (Private / Public) 바로 다음에 ref 키워드만 붙이면 끄읏!

public ref class MyClass
{
private:
public:
  MyClass()
  {

  }
}

헐 이렇게 쉬울 수가? 이렇게 선언된 클래스를 네이티브 클래스로 생성하려면 원래 사용하던 방식 그대로 클래스를 선언하면 그만이다. 자, 이제 궁금한 것은 C++/CLI 도 파괴자를 가지고 있느냐 하는 것인데 정답은 Yes다. 하지만 컴파일러는 파괴자 호출을 IDispose 인터페이스를 투명하게 구현한 다음 Dispose() 함수로 유도하게 된다. 추가적으로 GC에 의해 호출되는 Finalizer라고 불리는 녀석이 있는데 이것은 !MyClass() 와 같이 C++ 파괴자 함수 스타일이다. 이 Finalizer에서 사용자는 파괴자가 호출되었는지 체크해봐야 한다.


#using <mscorlib.dll>

using namespace System;

public ref class MyNamesSplitterClass
{
private:
  System::String ^_FName, ^_LName;
public:
  MyNamesSplitterClass(System::String ^FullName)
  {
    int pos = FullName->IndexOf(" ");
    if (pos < 0)
      throw gcnew System::Exception("Invalid full name!");
    _FName = FullName->Substring(0, pos);
    _LName = FullName->Substring(pos+1, FullName->Length - pos -1);
  }

  void Print()
  {
    Console::WriteLine("First name: {0}\nLastName: {1}", _FName, _LName);
  }
};

int main(array<System::String ^> ^args)
{
  // local copy

  MyNamesSplitterClass s("John Doe");
  s.Print();

  // managed heap

  MyNamesSplitterClass ^ms = gcnew MyNamesSplitterClass("Managed C++");
  ms->Print();

  return 0;
}

Value types

Value type이라 함은 기본으로 제공되는 타입말고 추가로 만들어내는 타입들을 말한다. 모든 value type들은 System::ValueType으로 부터 파생된다. 이 value type들은 스택에 저장될 수 있고 = 연산자를 통해서 할당 가능하다.


public value struct MyPoint
{
  int x, y, z, time;
  MyPoint(int x, int y, int z, int t)
  {
    this->x = x;
    this->y = y;
    this->z = z;
    this->time = t;
  }
};

Enums

마찬가지로 열거형도 다음과 같이 정의 가능하다.


public enum class SomeColors { Red, Yellow, Blue};

혹은 엘리먼트의 타입을 정의할 수도 있다.


public enum class SomeColors: char { Red, Yellow, Blue};


Arrays

배열의 선언은 사실 복잡해 보이지만 다음과 같이 쉽다.


cli::array<int> ^a = gcnew cli::array<int> {1, 2, 3};


기존 C++과의 차이라면 array<> 키워드가 쓰이고 있고 ^ 심볼로 핸들 지정을 하며 new 대신 gcnew를 사용한다는 점.


또 다른 형태의 배열 선언 방식은 다음과 같다.


array<int> ^a = gcnew array<int>(100) {1, 2, 3};

처음 방식보다 단순화 되었는데 cli 네임스페이스 명시를 빼고 100개의 배열을 선언한 다음 처음 3개만 값 1,2,3으로 초기화 했다. 각각의 배열 요소에 접근하려면 foreach를 다음과 같이 사용할 수 있다.


for each (int v in a)
{
  Console::WriteLine("value={0}", v);
}

자, 이제 4x5x2 형태의 3차원 배열을 선언해보자... 초기값은 모두 0.


array<int, 3> ^threed = gcnew array<int, 3>(4,5,2);

Console::WriteLine(threed[0,0,0]);

오오오... 생각보다 쉽다. 


이제 스트링 클래스를 배열로 선언해보자..


array<String ^> ^strs = gcnew array<String ^> {"Hello", "World"}


이런 형태를 초기화 하기 위해서는 다음과 같이 하면 된다.


array<String ^> ^strs = gcnew array<String ^>(5);
int cnt = 0;

// We use the tracking reference to access the references inside the array
// since normally strings are passed by value

for each (String ^%s in strs)
{
    s = gcnew String( (cnt++).ToString() );
}


나와 있는 설명처럼 % 심볼이 사용되었는데 그 이유는 일반적으로 스트링의 경우 값으로 전달되기 때문에 레퍼런스를 이용해야 메모리로 직접적인 접근이 가능하기 때문이다. 



Parameter Arrays

헛, 이것도 가능했단 말인가? C에서 printf() 함수와 같이 파라미터가 들어가는 함수의 사용도 가능하다. 단, 사용할 수 있는 조건은 C와 동일하다. 


using namespace System;

void avg(String ^msg, ... array<int> ^values)
{
  int tot = 0;
  for each (int v in values)
    tot += v;
  Console::WriteLine("{0} {1}", msg, tot / values->Length);
}

int main(array<String ^> ^args)
{
  avg("The avg is:", 1,2,3,4,5);
  return 0;
}


Properties

C#에서 사용되는 형태의 프로퍼티

public ref class Xyz
{
private:
  int _x, _y;
    String ^_name;
public:
  property int X
    {
      int get()
        {
          return _x;
        }
        void set(int x)
        {
          _x = x;
        }
    }
  property String ^Name
  {
    void set(String ^N)
    {
      _name = N;
    }
    String ^get()
    {
      return _name;
    }
  }
};


Wrapping Around a Native C++ Class

자, 이제 C++ class를 어떻게 CLI에서 감싸는지 보자


// native class

class Student
{
private:
  char *_fullname;
  double _gpa;
public:
  Student(char *name, double gpa)
  {
    _fullname = new char [ strlen(name+1) ];
    strcpy(_fullname, name);
    _gpa = gpa;
  }
  ~Student()
  {
    delete [] _fullname;
  }
  double getGpa()
  {
    return _gpa;
  }
  char *getName()
  {
    return _fullname;
  }
};

위 코드는 Native class의 한 예다. 이것을 C++/CLI 클래스로 바꾸려면 다음의 가이드를 따르면 된다.

  • Managed Class를 만들고 그것의 멤버 변수가 Native Class를 가리키도록 해라
  • 생성자 혹은 다른 적절한 위치에서 Native Class를 Native Heap (new 키워드를 사용하여) 에 생성해라
  • 생성자를 호출할 때 필요한 변수와 입력 인자를 전달하라. 이때 몇몇 입력 값은 Unmanaged 에서 Manged Type으로 Marshal (변환)이 필요할 것
  • Managed Class로 부터 드러내고 싶은 함수의 Stub를 생성하라
  • Managed Class의 파괴자에 Native Pointer 를 지우는 것을 잊지 마라 (메모리 누수)

자, 이제 위 Native Class의 C++/CLI 버젼을 보자.


// Managed class

ref class StudentWrapper
{
private:
  Student *_stu;
public:
  StudentWrapper(String ^fullname, double gpa)
  {
    _stu = new Student((char *) 
           System::Runtime::InteropServices::Marshal::StringToHGlobalAnsi(
           fullname).ToPointer(), 
      gpa);
  }
  ~StudentWrapper()
  {
    delete _stu;
    _stu = 0;
  }

  property String ^Name
  {
    String ^get()
    {
      return gcnew String(_stu->getName());
    }
  }
  property double Gpa
  {
    double get()
    {
      return _stu->getGpa();
    }
  }
};


중간에 좀 후덜덜한 부분이 있긴 하지만 그래도 생각보다 어렵진 않다.


Wrapping Around C Callbacks

여기서는 예제용으로 EnumWindows() API 함수를 Wrap 할 건데 다음이 코드의 핵샘이다.

  • Manged class를 하나 만드는데 Delegates와 함께 만들거나 Native Callback에 도달하면 호출되는 함수랑 만들거나.
  • 방금 만든 Managed Class로 향하는 레퍼런스를 가지는 Native Class 만든다. 이것은 vcclr.h 헤더에 있는 gcroot_atuo를 이용하면 가능하다
  • 이제 Native C Callback 함수를 만들고 넘길때 Context Parameter (우리 예제에서는 lParam) 로 넘긴다. (Native Class로의 포인터)
  • 이제 Native Callback 내부에서 Native Class 인 Context를 가지고 있으므로 이제 Managed Class로 향하는 레퍼런스를 얻어올수 있고 필요한 함수를 호출 할 수 있다.

using namespace System;

#include <vcclr.h>


// Managed class with the desired delegate

public ref class MyClass
{
public:
  delegate bool delOnEnum(int h);
  event delOnEnum ^OnEnum;

  bool handler(int h)
  {
    System::Console::WriteLine("Found a new window {0}", h);
    return true;
  }

  MyClass()
  {
    OnEnum = gcnew delOnEnum(this, &MyClass::handler);
  }
};


이제 Native Class를 만들어보자. 이 클래스 내부에는 Managed Class 레퍼런스 (m_clr)이 존재하고 직접적인 Callback 프로시져가 있다.


class EnumWindowsProcThunk
{
private:
  // hold reference to the managed class

  msclr::auto_gcroot<MyClass^> m_clr;
public:

  // the native callback

    static BOOL CALLBACK fwd(
    HWND hwnd,
    LPARAM lParam)
  {
      // cast the lParam into the Thunk (native) class,
      // then get is managed class reference,
      // finally call the managed delegate

      return static_cast<EnumWindowsProcThunk *>(
            (void *)lParam)->m_clr->OnEnum((int)hwnd) ? TRUE : FALSE;
  }

    // Constructor of native class that takes a reference to the managed class

  EnumWindowsProcThunk(MyClass ^clr)
  {
    m_clr = clr;
  }
};

마지막으로 실질적으로 사용하는 코드는,


int main(array<System::String ^> ^args)
{
  // our native class

  MyClass ^mc = gcnew MyClass();

    // create a thunk and link it to the managed class

  EnumWindowsProcThunk t(mc);

    // Call Window's EnumWindows() C API with the pointer
    // to the callback and our thunk as context parameter

  ::EnumWindows(&EnumWindowsProcThunk::fwd, (LPARAM)&t);

  return 0;
}


The Other Way Round: From Manged to C Callbacks

이 것은 더 쉽다. 왜냐면 이미 Managed Class 내부에 Native Class로 향하는 포인터를 가지고 있기 때문이다. 다음과 같이 하면 된다.

  • Native Callback를 트리거 (발생)시킬 적절한 Delegates를 가지는 Managed Class를 만든다
  • Native Class (Callback 을 포함하는)와 이전의 Managed Class (이벤트 발생시키는 놈)을 연결해줄 Managed Class를 만든다
  • Callback을 핸들링 할 Native Class를 만든다
예제용으로 TickGenerator라는 Managed Class를 만들었는데 이 TickGenerator라는 클래스는 OnTick이라는 이벤트를 만들어내고 INativeHandler 클래스 (인터페이스) 가 Managed Class인 TickGeneratorThuck로 부터 호출된다. MyNativeHandler 클래스는 사용자 자신의 핸들러을 어떻게 셋팅하는지 보여주기 위한 INativeHandler의 간단한 구현이다.


Tick Generator Delegate는 다음과 같다


public delegate void delOnTick(int tickCount);

그리고 Managed tick generator class는 다음과 같다.


ref class TickGenerator
{
private:
  System::Threading::Thread ^_tickThread;
  int _tickCounts;
  int _tickFrequency;
  bool _bStop;

  void ThreadProc()
  {
    while (!_bStop)
    {
      _tickCounts++;
      OnTick(_tickCounts);
      System::Threading::Thread::Sleep(_tickFrequency);
    }
  }

public:
  event delOnTick ^OnTick;

  TickGenerator()
  {
    _tickThread = nullptr;
  }

  void Start(int tickFrequency)
  {
    // already started

    if (_tickThread != nullptr)
      return;

    // p.s: no need to check if the event was set,
    // an unset event does nothing when raised!

    _tickCounts = 0;
    _bStop = false;
    _tickFrequency = tickFrequency;

    System::Threading::ThreadStart ^ts = 
      gcnew System::Threading::ThreadStart(this, &TickGenerator::ThreadProc);
    _tickThread = gcnew System::Threading::Thread(ts);
    _tickThread->Start();
  }
  
  ~TickGenerator()
  {
    Stop();
  }

  void Stop()
  {
    // not started?

    if (_tickThread == nullptr)
      return;
    _bStop = true;

    _tickThread->Join();
    _tickThread = nullptr;
  }
};


#pragma unmanaged
// Create a simple native interface for handling ticks

// Native classes implement this class to add custom OnTick handlers

class INativeOnTickHandler
{
public:
  virtual void OnTick(int tickCount) = 0;
};


class MyNativeHandler: public INativeOnTickHandler
{
public:
  virtual void OnTick(int tickCount)
  {
    printf("MyNativeHandler: called with %d\n", tickCount);
  }
};


#pragma managed
// Create the managed thunk for binding between the native
// tick handler and the tick generator managed class

ref class TickGeneratorThunk
{
private:
  INativeOnTickHandler *_handler;
public:
  TickGeneratorThunk(INativeOnTickHandler *handler)
  {
    _handler = handler;
  }

  void OnTick(int tickCount)
  {
    _handler->OnTick(tickCount);
  }
};


int main(array<System::String ^> ^args)
{
  // Initiate the native handler

  MyNativeHandler NativeHandler;

  // Create the tick generator class

  TickGenerator ^tg = gcnew TickGenerator();

  // Create the thunk and bind it with our native handler

  TickGeneratorThunk ^thunk = gcnew TickGeneratorThunk(&NativeHandler);

  // Bind the ontick event with the thunk's onclick event

  tg->OnTick += gcnew delOnTick(thunk, &TickGeneratorThunk::OnTick);

  // Start the tick generator

  tg->Start(1000);

  // Wait for user input

  Console::ReadLine();

  // Stop the generator

  tg->Stop();

  return 0;
}


글을 다 쓰고 보니 좀 글의 내용과 제목이 어울리지 않는다는 생각이 든다. 애초에 C++/CLI를 빠르게 습득하는 것을 타이틀로 했다면 특정 언어간의 호출에 치중하기 보다는 기본적인 타입의 사용과 다양한 예제를 통해 보다 일반적이고 기본적인 개념 설명을 하는 것이 더 좋았을듯 하다... 아무튼, 일단 글을 퍼오기는 했으니 저장은 하지만 썩 마음에 들지는 않는다. 물론 초반에 간단간단하고 명료한 설명은 참 좋았다...




posted by 대갈장군