블로그 이미지
Every unexpected event is a path to learning for you. blueasa

카테고리

분류 전체보기 (2797)
Unity3D (853)
Programming (479)
Server (33)
Unreal (4)
Gamebryo (56)
Tip & Tech (185)
협업 (61)
3DS Max (3)
Game (12)
Utility (68)
Etc (98)
Link (32)
Portfolio (19)
Subject (90)
iOS,OSX (55)
Android (14)
Linux (5)
잉여 프로젝트 (2)
게임이야기 (3)
Memories (20)
Interest (38)
Thinking (38)
한글 (30)
PaperCraft (5)
Animation (408)
Wallpaper (2)
재테크 (18)
Exercise (3)
나만의 맛집 (3)
냥이 (10)
육아 (16)
Total
Today
Yesterday

1. 알아두어야 할 이야기

 

C++/CLI를 사용하기 위해서는 Visual Studio 2005 이상의 버전이 설치되어 있어야 컴파일러가 C++/CLI 문법을 인식한다. C++/CLI를 이용하면 C# 의 장점과 C++의 장점을 동시에 취할 수 있다. Managed C++가 발전된 형태이다.

 

1.1. .Net Framework

C 언어가 널리 쓰였다는 점은 어떻게 보면 놀라운 일이다. C는 어려운 언어이기 때문이다. 강력한 반면 수 많은 메모리 관련 버그를 만들 수 있기 때문에 완벽하게 구사하기 어렵다. C++도 마찬가지로 좀 더 인간의 사고와 가까운 객체 지향의 패러다임을 도입하여 생각을 더 잘 표현해 낼 수는 있게 되고 클래스 재사용 등을 통해 생산성을 높였지만, 같은 문제를 지니고는 있었다. 아직까지도 탑을 차지하고 있는 프로그래밍 언어의 왕좌에 자극을 준 것은 자바이다. 자바는 가상머신 위에서 돌아가는 프로그램을 생산해내면서 플랫폼 사이의 호환 문제를 해결했을 뿐만 아니라, 더욱 높은 생산성을 가질 수 있게 되었다. 결과적으로 보면 자바를 따라 했다고 할 수 있겠는데, MS .Net Framework라는, 쉽게 말하면 라이브러리 셋을 만들어 내었다. (자바라는 언어가 없었다면 .Net Framework가 탄생할 수 있었을까?) .Net Framework을 통해 얻을 수 있었던 이점은 동일 프레임워크 상에서는 무조건 똑같이 수행될 바탕을 마련했다는 점과, 여러 유용하고 생산성을 증대시킬 수 있는 기능들을 미리 프레임워크 내에 구비시킴으로써 개발의 용이성을 증대시켰다는 점 등을 들 수 있겠다. 이를 위해 C#이라는 언어를 MS에서 등장시켜버렸다. 약간은 거부감까지도 가질 수 있도록 한 새로운 언어는 아마도 MS의 기대만큼은 인기를 얻지 못했다고 생각한다.

 

1.2. CLI (Common Language Infrastructure)

CLI(공통 언어 기반)MS.Net Framework, Mono, Potable .Net 과 같은 수많은 런타임의 중심부를 형성하는 실행 코드와 런타임 환경을 설명하기 위해 마이크로소프트사가 개발한 오픈 규격이다. 이 규격은 여러 개의 높은 수준의 언어가 다른 컴퓨터 플랫폼에서 특정한 구조를 위해 별도의 수정 과정 없이 사용될 수 있게 만들어 준다.

호환되는 모든 언어는 플랫폼 하드웨어에서 떨어져 나온 중간 언어인 공통 중간 언어(CIL)와 컴파일이 가능하다. 코드가 실행될 때, 플랫폼에 의지하는 VES는 공통 중간 언어를 호환 하드웨어에 맞추어 기계어로 컴파일하게 된다.

CLI 라이브러리는 웹서비스부터 3D 그래픽 프로그래밍까지 많은 부분의 기능을 제공하는데, 혹자는 MFC를 대체할 수 있을 정도라고까지 말한다. CLR(Common Language Runtime) CLI MS 버전으로 Windows에 맞도록 된 것이다.

 

1.3. Managed C++

MS는 개발자들을 감싸안기 시작했는데, .Net Framework 상에서 이루어지는 프로그래밍 언어의 특징을 Managed 라는 하나의 단어로 수렴시켜 그 특징들을 C++에 부여했다. 정확한 명칭은 Managed Extensions for C++이다. Visual Studio 내의 CLR(Common Language Runtime)을 이용하며 .NET 버전에서는 Managed라는 단어가 붙은 프로젝트들이 있었으며, 2005 이상에서는 공통 언어 런타임 지원(Common Language Runtime support) 옵션 중에 /clr:oldSyntax을 택해야 빌드 가능하다. 그냥 /clr C++/CLI를 위해 존재한다. 이미 Managed C++ Visual Studio 2008의 등장으로 막을 내렸다.

 

1.4. C++/CLI

Managed C++은 장점을 가려버릴 수 있는 단점들도 가지고 있었는데, 포인터를 나타내는 *과 같은 표현은 Managed C++에서의 참조 타입과 혼동을 일으킬 수 있었고, 이미 존재하는 키워드 들을 피하기 위해 __와 같은 표현을 사용하였는데, 개발자들이 매우 싫어하였다. C++/CLI는 이러한 점들을 고려하여 디자인 되었다그리고 Managed C++ MS 혼자 개발하였다면, C++/CLI CLI 라이브러리를 적극적으로 사용하여 여러 플랫폼에서도 공통적으로 사용할 수 있도록 되었다. C++/CLI C# CLI와 마찬가지로 유럽 전자계산기 공업회(ECMA)의 아래에서 표준화를 진행되고 있어 궁극적으로는 ISO의 아래에서도 표준화 되게 될 것이다.

 

 

2. 시작하기

 

2.1. 프로젝트 생성

2.1.1. CLR 프로젝트 생성

기본적으로 CLR을 사용하는 C++ 프로젝트로 C++/CLI를 지원함

2.1.2. 일반 C++ 언어를 사용하는 프로젝트 :

/clr 옵션 설정하고 빌드 (프로젝트 속성에서 공통 언어 런타임(Common Language Runtime)에서 선택) /clr 옵션을 사용할 때 설정해 주어야 하는 옵션 들에 대해 자세히 살펴본다.

2.1.2.1. /clr 옵션

전체 프로젝트에 적용하기 보다는 .cpp 파일에 따로 설정한다. /clr 옵션은 ‘/Yu:미리 컴파일된 헤더 사용’, ‘/Gy:함수 수준 링크 사용등의 몇몇 컴파일 옵션과 호환되지 않으므로 이전보다 빌드 속도가 떨어지기 때문이다.

 

위 그림과 같이 프로젝트 전체가 아니라 파일의 속성을 변경하도록 한다.

 

 

위 그림과 같이 Compile with Common Language Runtime Support 항목에서 /clr을 선택한다.

 

2.1.2.2. /Yu, /Gy 옵션

두 옵션은 /clr 옵션과 호환되지 않는다. 따라서 설정되어 있다면 옵션을 제거하도록 한다.

 

Enable Function-Level Linking : Yes(/Gy) -> No

Create/Use Precompiled Header : Use Precompiled Header (/Yu) -> Not Using Precompiled Headers

 

 

위와 같이 설정하여 준다.

 

2.1.2.3. /ZI, /EHsc, /RTC 옵션

3가지 옵션들은 경험상 디버그로 빌드시 /clr 옵션과 충돌나는 것들을 모아놓은 것이다. 각각 다음과 같이 바꾸어 주어야 한다.

 

Enable C++ Exceptions : /EHSc -> /EHa

Basic Runtime Checks : Both (/RTC1, equiv. to /RTCsu) -> default

 

각각의 위치는 다음과 같다.

 

 

 

 

 

2.1.3. MFC와 함께 사용하기

MFC와 함께 사용하기 위해서는 추가적으로 더 작업이 필요하다.

 

Use of MFC : Use MFC in a Static DLL -> Use MFC in a Shared DLL

Debug Information Format : ZI -> Zi

Runtime Library : Any -> Multi-threaded DLL (/MD) or Multi-threaded Debug DLL (/MDd)

 

그런데 중요한 사실은 MFC Static DLL로 사용할 수 없고, Shared DLL로만 사용 가능하다는 사실이다 .따라서 MFC와 함께 사용할 때는 따로 DLL로 만들어서 호출을 하는 것이 좋겠다.

각각의 위치는 다음과 같다.

 

 

 

 

 

 

2.2. 기본적으로 추가해야 할 파일

.Net 프로그래밍을 위해 추가되어야 할 파일

C#과 마찬가지이다.

 

#using <mscorlib.dll>

 

2.3. C++의 헤더 파일

Native C++처럼 그대로 사용 가능하다.

 

#include <stdio.h>

 

2.4. C#의 기본 클래스

C#처럼 사용하지 못하고, 상위 클래스와 하위 클래스 사이에 .대신 ::를 넣어서 사용한다.

 

using namespace System;
using namespace System::Runtime::InteropService;
// using namespace System.Runtime.InteropService; in C#
 
System::Console::WriteLine(“Hello”);
// System.Console.WriteLine(“Hello”); in C#

 

2.5. C++/CLI 처리 라이브러리

다음과 같이 필요한 dll들을 참조한다.

 

#using <mscorlib.dll>
#using <system.dll>
#using <system.drawing.dll>

 

2.6. 문법

기존의 C++ 코드를 그대로 사용 가능하고, C++/CLI에서 제공하는 기능을 이용하고 싶을 때에는 문서의 아래에서 설명하게 될 C++/CLI 문법을 사용한다.

명시적으로 나누어줄 수도 있는데, 다음과 같이 #pragma managed, #pragma unmanaged 를 사용해서 명시해 줄 수 있다.

 

#using <mscorlib.dll> // Console::WriteLine 사용하기 위해

 

#include <stdio.h> // printf() 사용하기 위해

using namespace System;

 

// 여기서부터는 unmanaged 코드

#pragma unmanaged

void print(char *msg)

{

  printf("%s\n", msg);

}

 

// 다시 managed 코드로

#pragma managed

int main()

{

  // managed 코드에서 콘솔에 출력

  Console::WriteLine("Hello world from managed method");

 

  // stdio 이용하여 콘솔에 출력

  print("hello world from unmanaged method");

return 0;

}

 

 

3. 객체(클래스)

 

3.1. 핸들과 포인터

포인터를 Native C++에서 흔히 쓰던 메모리의 주소값을 지니는 변수라고 하면, 핸들은 객체를 참조하는 변수라고 할 수 있으며 “safe pointer”라고 불리기도 한다. 가장 큰 차이는 핸들로 참조된 객체는 managed heap에 할당되고 Garbage Collector에 의해 관리되어 메모리 누수 등이 발생하지 않는다.

 

3.2. ^연산자

핸들을 나타낸다. “gcnew” 키워드를 이용해 할당한다.

 

System::Object ^x = gcnew System::Object();
 
N* pn = new N; // native heap 할당
N& rn = *pn; // ordinary reference native object 바인딩
R^ hr = gcnew R; // CLI heap 할당
R% rr = *hr; // tracking reference gc-lvalue 바인딩

 

3.3. CLI 타입

3.3.1. value type

stack에 할당 (할당과 제어의 효율성))

“value” 키워드 사용. = 오퍼레이터 사용하여 값을 넣는다.

 

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;
}
}

 

3.3.2. reference type

managed heap에 할당 (객체 지향 프로그래밍에서의 클래스의 특징)

reference 타입의 객체는 기본적으로 헤더가 있는데, 이 헤더는 가상 메소드를 위한 클래스 계층도 등의 객체 지향 프로그래밍의 기본 서비스가 가능토록 하고, 모든 종류의 용도에 붙게될 메타데이터를 제공토록 한다.

 

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

 

3.4. 메모리 관리

GC(Garbage Collector)에 의한 메모리 관리. 객체를 따로 delete 하지 않아도 된다.

어플리케이션이 더 이상 그 메모리를 참조하지 않아 GC가 결정내릴 때에 수거된다.

메모리 할당 성능이 더 좋고 참조의 격리성이 보장되는 이점도 있다.

 

3.5. 리소스 관리

(unmanaged 리소스 해제를 위한 지저분하기 짝이 없는, 그러나 반드시 알아야 하는내용이다. 좀 길지만 잘 숙지해야 한다.)

객체가 할당되었을 때, 사용 가능 메모리가 대부분 없어지면 GC가 수거하여 이를 관리한다. 이를 GC의 다이내믹 메모리 관리라고 한다. GC가 메모리를 해제할 때는 Finalize라는 메소드를 찾아보고 있으면 호출한다. 그러나 객체 내의 리소스, 즉 어떠한 종류의 락 또는 Native heap 메모리, 등의 기간 자원이 차지하고 있을 때, 다이내믹 메모리 관리는 효력을 발휘할 수 없다. 이때 리소스 자원은 필요없게 될 때 그 자원을 곧바로 해제하는 방법이 제일 유리하다. 현재 CLI에서 지원하는 해결책은 IDisposable 인터페이스의 Dispose 메소드를 구현하는 것이다. 문제는 이 경우, Dispose 메소드를 명시적으로 호출해야 한다는 점이다.

결론은 이렇다. C++/CLI에서는 Dispose 메소드를 소멸자로 매핑시킨다. 이로써 프로그래머는 좀 더 자연스러움을 느낄 수 있다. 여기에는 가비지 콜렉터가 그 객체에 해당하는 Finalize 메소드 호출을 막는 일도 포함된다. Finalize 메소드는 모든 자원을 해제하는 역할을 하는 실질적인 메소드라고 할 수 있다. 따라서 C++/CLI 설계시 소멸자 안에서 Finalize 메소드를 호출하는 것이 기본이다.

이제 소멸자가 자동으로 호출되게 하는 것이 필요한데, 이것을 지원하는 특별한 스택 기반 표기법이 있다. 그것은 객체의 lifetime이 그것의 선언의 유효 범위로 한정되게 한다. 내부적으로 컴파일러는 managed heap에 객체가 할당되도록 한 후, 유효 범위의 끝에 소멸자로 작성된 Dispose 메소드를 삽입하여 자동으로 호출되게 한다.

Finalize 메소드는 !가 클래스 이름에 붙은 형태로 선언되는데, 컴파일러가 역시 Finalize 메소드와 매핑시켜 준다.

이렇게 리소스를 사용하는 객체는 두 가지 메소드를 모두 선언해 놓아야 리소스 해제를 보장받을 수 있다. 항상 모든 케이스를 생각해야 하는 C++보다는 쉬워진 면이 있으나 소멸자와 Finalize를 반드시 구현해야 하는 점은 마찬가지인 것 같다. 아래의 코드를 보면 훨씬 이해가 쉬워질 것이다.

 

ref class Wrapper {
  Native *pn;
public:
  // pn Native heap 할당된 메모리
  Wrapper( int val ) { pn = new Native( val ); } 
 
  // Native heap 메모리에 대한 Dispose 함수
  ~Wrapper(){ this->!Wrapper(); }
 
  void mfunc();
protected:
 
  // 메모리 해제의 실패에 대비한 명시적인 Finalize 함수
  !Wrapper() { delete pn; }
};
 
void f1() 
{
  // 일반적인 참조 타입
  Wrapper^ w1 = gcnew Wrapper( 1024 );
 
  // lifetime 매핑된 참조 타입
  Wrapper w2( 2048 ); // ^(cap) 없다.
 
  w1->mfunc(); 
  w2.mfunc();
 
  // w2 아마도 여기서 Dispose 되어 메모리가 해제될 것이다.
  // finalize 호출되지는 않는다.
}
 
// 
// ... 후에, w1 여기서 finalize 호출되어 메모리가 해제될 것이다.

 

다음은 이와 관련된 추가적인 지식이다.

·       C++/CLI에서의 소멸자는 virtual 키워드가 없더라도 무조건 가상 함수이다.

·       finalizer의 가시성 범위는 accessor가 있건 없건 private이다.

·       destructor finalizer IDisposable::Dispose Finalize()를 완전 대체하지는 않는다. 컴파일러는 IL 코드 내에 Dispose() Finalize()를 따로 삽입하며, 각기 내부에서 destructor finalizer를 적절히 호출한다.

·       destructor가 호출되면 finalizer는 호출되지 않는다. 이는 IL 코드 내 Dispose() 구현에서 GC.SupressFinalizer()를 호출하기 때문이다.

·       위 코드를 기반으로 한 컴파일된 IL 코드는 C# 버전과 거의 흡사하다(Dispose(bool)을 통한 파생 클래스에서의 리소스 정리 등).

·       위와 같은 내용을 기반으로, 파생 클래스에서 역시 위 패턴과 동일하게 작성하면 된다(destructor 또는 Finalizer 등에서 base 클래스의 destructor/finalizer 명시적 호출 등 부가적 행동 불필요).

 

4. 프로그래밍 요소

 

4.1. Property

Property는 말 그대로 객체의 속성을 나타내는 지역변수라고 할 수 있다. 이는 C++만 다루었던 프로그래머에겐 생소하지만 다른 언어들이 많이 가지고 있는 property를 지원하기 위함인 듯싶다. CLI에서의 property get/set 멤버 함수를 통해 값을 얻고, 값을 설정하는 가상 데이터 멤버라고 볼 수 있다. 다음과 같은 syntax를 가지고 있으며 사용 방법도 볼 수 있다.

 

ref class R { 
 
public: 
property int Size { 
  int get() { return size_; } 
  void set( int val ) { size_ = val; } 
}   
 
private: 
int size_; 
};
 
void f1() 
{
  Size = 1; // Size.set 호출
int S = Size; // Size.get 호출
}

 

4.2. Boxing

value 타입을 reference 타입으로 바꾸는 것이 Boxing이다. 예를 들어 정수 값을 가지는 값 타입이 있을 때, 그것을 문자로 표현하고 싶다면 물론 문자열을 만들어서 format 변환을 한다던지 할 수 있지만, 정수 값을 가지는 reference 타입의 객체라면 간단히 ToString() 메소드를 써서 사용할 수 있다.

 

int C = 10; //value 타입
int^ refC = C; //boxing 통해 reference 타입으로 거듭나다.
String msg = “C  : “ + refC.ToString(); //이런 식으로 사용이 가능하다.
int D = *refC;
refC = nullptr; //핸들을 0으로 초기화 한다.

 

좀 더 자세히 알아보면 boxing이란 값과 객체간의 빈틈을 연결짓는 메카니즘이다. 비록 CLR은 모든 타입이, 간접적이건 직접적이건 간에, Object에 서 파생될 것을 요구하지만, 사실 값에 대해서는 그렇지가 않다. 스택에 존재할 정수와 같은 단순 값은 단지 컴파일러가 특정 연산을 가능케 하는 하나의 메모리 블록일 뿐이다. 만약 값을 객체처럼 다루고 싶다면, 그 값은 반드시 객체가 되어야 한다. 그 값은 Object에서 파생된 메소드를 제공할 수 있어야 하기 때문이다. boxing이란 개념은 바로 이를 가능케 하기 위해 CLR이 제공하는 메카니즘이다.

 

4.3. Delegate

Delegate C++의 함수 포인터와 같은 역할을 수행해서 콜백함수와 같은 구현을 할 때 쓸 수 있다. Delegate는 객체 내의 하나 또는 그 이상의 메소드와 바인딩 될 수 있다. Delegate는 두 개의 파라미터를 가지는데, 하나는 객체이고, 나머지 하나는 그 객체 내의 함수이다.

 

delegate void Function();
 
ref struct ReferenceType
{
    void InstanceMethod() {}
    static void StaticMethod() {}
};
 
// 객체와 객체 내의 함수를 바인딩 시켜 delegate 생성한다.
Function^ f = gcnew Function(gcnew ReferenceType,
                             &ReferenceType::InstanceMethod);
 
// static 멤버 gatneh delegate 추가한다.
f += gcnew Function(ReferenceType::StaticMethod);
 
//  함수를 한꺼번에 호출한다.
f();
 
//다음과 같이 IEnumerator 사용하여 하나씩 호출  수도 있다.
array<System::Delegate^, 1>^ d = f->GetInvocationList();
 
IEnumerator^ e = d->GetEnumerator();
int idx = 1;
while (e->MoveNext())
{
   Function ^delegateI = 
            dynamic_cast<Function ^>(e->Current);
   delegateI->Invoke();
   idx++;
}

 

4.4. Enum

다음과 같이 enum을 사용 가능하다.

 

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

 

또한 타입을 정해주는 것도 가능하다.

 

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

 

4.5. Arrays

다음과 같은 syntax들이 가능하다.

 

cli::array<int> ^a = gcnew cli::array<int> {1, 2, 3};
array<int> ^a = gcnew array<int>(100) {1, 2, 3};
//for each 구문 사용하기
for each (int v in a)
{
  Console::WriteLine("value={0}", v);
}

 

또한 다음과 같은 복잡하거나 특별한 Array도 사용 가능하다.

 

//multi dimension Array
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"}

 

 

4.6. C#으로부터 인자값 받기

int, short 같은 경우는 그냥 받으면 된다.

 

void func(int a, short b){ }

 

string 형식 받기

 

void func(String^ _name_){ }

 

구조체 받기

 

void func(testStruct^% _name_){ }
_name_->member = 10;

 

C#에서는 func(ref teststruct); 이렇게 호출해야 한다

 

구조체 배열 받기

 

void func(array<testStruct^>^% _name_){ }

 

이때 C#에서는 func(ref teststructArray); 이렇게 호출해야 한다.

 

4.7. C++ 포인터로 옮기기

4.7.1. 배열

 

array<int>^ arr =  gcnew array<int>(SIZE);

pin_ptr<int> p = &arr[0];   // arr 첫번째 요소를 pin pointer 대입
int* np = p;   // arr
첫번째 요소를 포인터에 대입

native_function(np);   // native 함수에 포인터를 넘김

 

4.7.2. 문자열

 

String^ str = gcnew String("abcde"); //Managed String 생성
 
TCHAR* pstr =  static_cast<TCHAR*>(System::Runtime::InteropServices::Marshal::StringToHGlobalUni(str).ToPointer()); //Inerop 서비스를 이용한 문자열 변환

 

4.7.3. 구조체

 

//Managed class 생성
ref class TestClass
{
public:
     String^ A;
     UInt32 B;
     cli::array<byte>^ C;
 
public:
     TestClass(String^ _A, UInt32 _B, cli::array<byte>^ _C)
     {
             A = _A;
             B = _B;
             C = _C;
     }
};
//Umanaged 코드로 변경
#pragma unmanaged 
 
//C++ 구조체
struct TestStruct
{
     TCHAR *A;
     UINT B;
     BYTE *C;
};
 
//다시 Managed 코드로 변경
#pragma managed
 
TestStruct* copystruct()
{
     cli::array<byte>^ _bytes = gcnew cli::array<byte>(128);
 
     TestClass^ TT = gcnew TestClass("test", 1, _bytes);
 
     struct TestStruct* gg = (struct TestStruct*)malloc(sizeof(struct TestStruct));
 
     //각각 형식에 맞게 하나하나 대입
     gg->A = static_cast<TCHAR*>(System::Runtime::InteropServices::Marshal::StringToHGlobalUni(TT->A).ToPointer());
     gg->B = TT->B;
     pin_ptr<byte> pC= &TT->C[0];
     gg->C = pC;
     return gg;
}

 

4.7.4. System::Runtime::InteropServices::Marchal

마샬링 : 마샬링은 하나 이상의 프로그램 또는 연속되어 있지 않은 저장 공간으로부터 데이터를 모은 다음, 데이터들을 메시지 버퍼에 집어넣고, 특정 수신기나 프로그래밍 인터페이스에 맞도록 그 데이터를 조직화하거나, 미리 정해진 다른 형식으로 변환하는 과정을 말한다. 마샬링은 대체로, 어떤 한 언어로 작성된 프로그램의 출력 매개변수들을, 다른 언어로 작성된 프로그램의 입력으로 전달해야 하는 경우에 필요하다.

 

C++, C++/CLI간 데이터 변환에 관해서 Mashal 클래스를 주로 이용하도록 한다.

 

 

1 C++, C# 비교 요약

설명

C++/CLI

C#

참조 타입 할당

ReferenceType^ h
  = gcnew ReferenceType;

ReferenceType h
   = new ReferenceType();

값 타입 할당

ValueType v(3, 4);

ValueType v
   = new ValueType(3, 4);

참조 타입의 스택 의미론

ReferenceType h;

N/A

Dispose 메서드 호출

ReferenceType^ h
  = gcnew ReferenceType;

delete h;

ReferenceType h
   = new ReferenceType();

((IDisposable)h).Dispose();

Dispose 메서드 구현

~TypeName {}

void IDisposable.Dispose()

Finalize 메서드 구현

!TypeName {}

~TypeName {}

박싱

int^ h = 123;

object h = 123;

언박싱

int^ h = 123;
int c = *h;

object h = 123;
int i = (int)h;

참조 타입 정의

ref class ReferenceType {};
ref struct ReferenceType {};

class ReferenceType {}

값 타입 정의

value class ValueType {};
value struct ValueType {};

struct ValueType {}

속성 사용하기

h.Prop = 123;
int v = h.Prop;

h.Prop = 123;
int v = h.Prop;

속성 정의하기

property String^ Name
{
    String^ get()
    {
        return m_value;
    }
    void set(String^ value)
    {
        m_value = value;
    }
}

string Name
{
    get
    {
        return m_value;
    }
    void set
    {
        m_value = value;
    }
}

 

 

5. WCF C++/CLI

 

5.1. C++에서 WCF 사용하기

C++ WCF가 연동되어야 하는 이유는 굳이 이야기 하지 않아도 될 것 같다. 기존의 시스템과 연동되게 하기 위해서이다. 몇 가지 방법이 있는데 방법들에 대한 장단점만 나열해 보겠다.

 

방법

장점

단점

SProxy.exe

클라이언트만 구현 가능

Windows 플랫폼만 가능

Basic HTTP만 지원

WWSAPI

API 레벨 지원

클라이언트/서버 모두 구현 가능

Windows 플랫폼만 가능

Basic HTTP/wsHTTP 지원

gSOAP

클라이언트/서버 모두 구현 가능

플랫폼 독립성

Basic HTTP만 지원

COM Moniker

순수 Unmanaged 코드 사용
(/clr 옵션 적용 필요 없음)

Windows 플랫폼만 가능

.NET 3.0 이상 필요

COM Interop 사용

C++/CLI

WCF의 모든 기능 지원

클라이언트/서버 모두 구현 가능

Windows 플랫폼만 가능

.NET 3.0 이상 필요

/clr 옵션을 적용해야 함

 

앞으로 등장할 WWSAPI는 윈도우 플랫폼에서의 최상의 선택이 될 것 같지만, 일단 현재로써는 C++/CLI가 가장 좋은 선택일 것 같다.

 

5.2. 실제로 연동하기

WCF를 이용하려면 먼저 .NET Framework System.ServiceModel.dll을 이용해야 한다. C++/CLI코드를 이용하여 .NET의 컴포넌트들을 이용해야 하는데, 사용자가 직접 코딩을 할 필요는 없다. 서비스를 설명하는 WSDL 문서를 얻을 수만 있다면, WSDL로부터 프록시 코드를 얻을 수 있는데, 이 프록시 코드를 프로젝트에 포함시키고 클래스를 사용하기만 하면 된다. 단계별로 살펴보도록 하겠다.

 

5.2.1. 서비스 만들기

다음은 DB에서 고객 리스트를 검색하여 반환해주는 서비스를 제공하는 WCF 서비스 코드로 C#으로 작성되었다. 서비스 코드에 대한 설명은 생략하도록 하겠다.

 

//계약
[ServiceContract]
public interface ICustomer
{
     [OperationContract]
     void getUserList(ref List<USER_INFO> userList);
}
 
public class CustomerService : ICustomer
{
     //DB 연결하여 검색하는 라이브러리
     [DllImport("DBCon.dll")]
     private static extern uint getUserCount();
     [DllImport("DBCon.dll")]
     private static extern void getData(uint idx, ref USER_INFO aUser);
 
     public void getUserList(ref List<USER_INFO> userList)
     {
              List<USER_INFO> m_userList = new List<USER_INFO>();
 
              uint DataSize = getUserCount();
 
              for (uint i = 0; i < DataSize; i++)
              {
                      USER_INFO aUser = new USER_INFO();
                      getData(i, ref aUser);
                      m_userList.Add(aUser);
              }
              userList = m_userList;
     }
}

 

5.2.2. 서비스 호스팅

서비스는 그야말로 실제로 요청 받은 작업을 수행하는 모듈로 서비스는 적당한 네트워킹 서비스에 의해 호스팅 되어야 한다. 네트워킹 서비스는 사용자가 어플리케이션으로 작성할 수도 있고, IIS의 도움을 받을 수도 있고, 윈도우 서비스로 받을 수도 있다. 중요한 것은 서비스를 제공하는 방법을 binding endpoint로 정의한 내용인데, 호스팅을 하는 모듈의 코드에 직접 하드코딩 할 수도 있고, Configuraton 파일을 사용할 수도 있다. 다음은 윈도우 서비스로 호스팅을 하고 있는 서비스의 내용을 설명하고 있는 Configuration 파일이다.

 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <services>
      <service name="Company.CustomerService"
           behaviorConfiguration="httpGetbehavior">
        <host>
          <baseAddresses>
            <add baseAddress="http://192.168.3.65/CustomerService"/>
          </baseAddresses>
        </host>
        <endpoint
          contract="Company.ICustomer"
          address=""
          binding="basicHttpBinding"
        />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="httpGetbehavior">
          <serviceMetadata httpGetEnabled="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

 

여러가지 사항은 제외하고 serviceBehaviors httpGetbehavior의 이름을 가지고 있는 속성을 서비스에 behaviorConfiguration으로 등록해 주어야 WSDL 파일을 http 프로토콜의 Get 방식으로 얻을 수 있다. 이렇게 얻은 WSDL 코드는 다음과 같다.

 

<?xml version="1.0" encoding="utf-8" ?> 
<wsdl:definitions name="CustomerService" targetNamespace="http://tempuri.org/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://tempuri.org/" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract" xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:wsa10="http://www.w3.org/2005/08/addressing" xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex">
  <wsdl:types>
    <xsd:schema targetNamespace="http://tempuri.org/Imports">
      <xsd:import schemaLocation="http://192.168.3.65/CustomerService?xsd=xsd0" namespace="http://tempuri.org/" /> 
      <xsd:import schemaLocation="http://192.168.3.65/CustomerService?xsd=xsd1" namespace="http://schemas.microsoft.com/2003/10/Serialization/" /> 
      <xsd:import schemaLocation="http://192.168.3.65/CustomerService?xsd=xsd2" namespace="http://schemas.datacontract.org/2004/07/Company" /> 
  </xsd:schema>
  </wsdl:types>
  <wsdl:message name="ICustomer_getUserList_InputMessage">
    <wsdl:part name="parameters" element="tns:getUserList" /> 
  </wsdl:message>
  <wsdl:message name="ICustomer_getUserList_OutputMessage">
    <wsdl:part name="parameters" element="tns:getUserListResponse" /> 
  </wsdl:message>
  <wsdl:portType name="ICustomer">
    <wsdl:operation name="getUserList">
      <wsdl:input wsaw:Action="http://tempuri.org/ICustomer/getUserList" message="tns:ICustomer_getUSerList_InputMessage" /> 
      <wsdl:output wsaw:Action="http://tempuri.org/ICustomer/getUserListResponse" message="tns:ICustomer_getUserList_OutputMessage" /> 
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="BasicHttpBinding_ICustomer" type="tns:ICustomer">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" /> 
    <wsdl:operation name="getUserList">
      <soap:operation soapAction="http://tempuri.org/ICustomer/getUserList" style="document" /> 
      <wsdl:input>
        <soap:body use="literal" /> 
      </wsdl:input>
      <wsdl:output>
        <soap:body use="literal" /> 
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="CustomerService">
    <wsdl:port name="BasicHttpBinding_ICustomer" binding="tns:BasicHttpBinding_ICustomer">
      <soap:address location="http://192.168.3.65/CustomerService" /> 
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

 

5.2.3. 프록시 코드 생성하기

프록시 코드는 svcutil.exe 라는 .NET 프레임워크에서 제공하는 유틸을 사용한다. svcutil.exe를 사용하면 WSDL 파일을 읽어서 선택한 언어로 된 프록시 코드를 생성해 준다. 다음과 같이 쓴다.

 

cmd> svcutil.exe http://192.168.3.65/CustomerService /language:C++ /out:proxy

 

이렇게 하면 해당 웹서비스를 이용할 수 있는 클래스가 정의된 프록시 코드가 proxy.h라는 이름으로 생성되고, 함께 사용 가능한 output.config 파일이 생성된다. Proxy.h 코드를 보면 알겠지만 C#에서의 List<USER_INFO>와 같은 경우 Array<USER_INFO>^ 와 같이 나름대로 해석해서 정의되어 있는 것을 볼 수 있다. 프록시 코드는 너무 길어서 첨부 생략한다.

 

5.2.4. 프록시 코드 사용하기

C++/CLI로 생성된 프로젝트(혹은 C++로 생성된 프로젝트에 /clr 옵션을 준 경우) proxy.h를 추가해서 빌드해 본다. 빌드 오류가 나지 않는다면 다음과 같이 코딩 할 수 있다.

 

int _tmain(int argc, _TCHAR* argv[])
{
     //proxy.h 내의 binding 이름을 가지고 클라이언트를 생성한다.
     CustomerClient^ Client = gcnew CustomerClient(_T("BasicHttpBinding_ICustomer"));
 
     //리스트의 개수는 128개라고 가정
     array<USER_INFO>^ userList = gcnew array<userList>(128);
 
     //메소드를 호출한다.
     Client->getUserList(userList);
}

 

 

6. References

 

A Design Rationale for C++/CLI

http://www.gotw.ca/publications/C++CLIRationale.pdf

C++: The Most Powerful Language for .NET Framework Programming

http://msdn.microsoft.com/en-us/library/ms379617.aspx

Quick C++/CLI - Learn C++/CLI in less than 10 minutes

http://www.codeproject.com/KB/mcpp/cpptomancpp.aspx

FunctionX C++/CLI Tutorial

http://www.functionx.com/cppcli/index.htm

Pure C++ Hello, C++/CLI

http://msdn.microsoft.com/en-us/magazine/cc163681.aspx

C++/CLI Quick reference

http://jundols.springnote.com/pages/1721142

How to: Compile MFC and ATL Code with /clr

http://msdn.microsoft.com/en-us/library/ms235211(VS.80).aspx




반응형
Posted by blueasa
, |