저는 제 컴퓨터의 E:\Projects\HalfNetwork 에
압축을 풀어서 안에 ExternalLib 폴더와 HalfNetwork
폴더가 만들어졌습니다.
HalfNetwork 빌더하기
E:\Projects\HalfNetwork\HalfNetwork 폴더에
가면 VS 솔루션 파일이 있습니다. 그 중 VS2010 솔루션인 HalfNetwork_vc10.sln 을 선택합니다.
프로젝트를 열면 ‘HalfNetwork’ 프로젝트를 ‘시작 프로젝트’로 선택합니다.
C++에서만 사용하는 경우라면 그대로 빌드를 하면 됩니다. 그러나 (방법 1)C++/CLI에서
사용하기 위해서는 프로젝트 속성에서 [C/C++] – [코드 생성]에서
‘런타임 라이브러리’ 값을 ‘다중 스레드 DLL’로 바꾸어야 합니다.
C++/CLI에서만 사용하지 않을 생각이라면 구성을 하나 더
만들기를 추천합니다.
(방법 2)저는 기존의
구성에 ‘DebugMDd’(디버그 모드의 경우)라는 것을
만들어서 ‘런타임 라이브러리’ 값을 바꾸었습니다.
그리고 기존 구성에서 만들어진 출력 파일과 이름이 겹치지 않도록 이름도 변경하도록 합니다.
(Debug 모드에서의 대상 이름 속성의 값은 ‘$(ProjectName).x86.debug’였습니다)
그럼 이제 빌드를 합니다. VS2010만 제대로 설치되어 있다면
아무런 문제 없이 빌드가 될 것입니다. C++/CLI에서 사용하기 위해서 약간 수정을 했지만 만약 C++에서만 사용한다면 HalfNetwork는 솔루션 파일을 연 후
‘빌드’를 선택하는 것으로 준비는 끝납니다.
빌드가 끝나면 E:\Projects\HalfNetwork\HalfNetwork\Bin32
폴더에 lib 파일이 만들어져 있습니다.
저는 (방법 2)로 빌드를 했기 때문에 HalfNetwork.x86.debugMDd.lib 라는 파일이 만들어졌습니다.
이것으로 HalfNetwork lib 파일을 만드는 것은 끝났습니다.
다음 회에서는 이번에 만든 lib 파일을 사용하여 닷넷용 클래스 라이브러리를 만들어 보겠습니다.
ACE는 정말 좋은 네트웍 프레임웍이지만 너무 범용적이라서 고도로
추상화 되었고, 기능이 다양하여 분석하기가 쉽지 않아서 사용에 어려움이 있습니다.
HalfNetwork는 javawork님이 다년간 온라인 게임 서버를
만든 경험을 바탕으로 서버 프로그램을 만들 때 필수적이고 자주 사용하는 기능을 쉽게 사용할 수 있도록 만든 것으로 서버와 클라이언트 용 둘 다
지원합니다. 그래서 ACE의 장점이 높은 성능과 다양한 기능, 멀티 플랫폼 지원을 이어 받으면서 아주 쉽게 서버 프로그램을 만들 수 있습니다(즉 ACE를 몰라도 괜찮습니다).
C++/CLI를 가장 자주 사용하는 경우가 아마 C++로 만든 코드를 닷넷에서 사용하고 싶을 때라고 생각합니다. 게임
개발에서는 보통 3D 툴을 만들 때 C++/CLI를 유용하게 사용합니다.
C++/CLI를 툴 개발에서 사용할 때는 보통 두 가지로 사용하는데
첫 번째는 C++로 만든 3D 엔진 코어를 DLL로 만들어서 C#을 이용하여 툴을 만들던가, 두 번째는 C++/CLI에서 관리코드와 비관리코드를 같이 사용할 때입니다.
제가 앞서 C++/CLI에 대해서 설명한 것들은 C++/CLI로만 프로그래밍 하는 것보다는 관리코드와 비관리코드를 같이 사용할 때를 생각하여 이때 필요로 하는
부분을 중심으로 했습니다. 그래서 아직 C++/CLI에 대한 많은
부분들이 빠져있습니다. 빠진 부분에 관심이 있는 분들은 C++/CLI
책이나 MSDN을 통해서 공부하시기 바랍니다.
앞으로 C++로 만든 코드를 C++/CLI를 사용하여 닷넷용 클래스 라이브러리를 만든 후 이것을 C#에서
사용하여 프로그램을 만든다는 경우로 간단한 프로그램을 만들면서 C++/CLI을 어떻게 사용하는지 몇 회에 걸쳐서 설명하겠습니다.
만들 프로그램은 네트웍 서버 애플리케이션입니다(제가 회사에서
맡은 직무가 온라인 게임 서버 개발입니다). ^^
네트웍 라이브러리(또는 프레임웍)
네트웍 프로그래밍은 일반적인 프로그래밍은 아니고 시스템 프로그래밍입니다. 조금은 특수한 분야라고 할 수 있습니다.
그러나 한국의 게임업계는 거의 대부분이 온라인
게임이라서 네트웍 프로그래밍이라는 것이 특수한 부분은 아닙니다.
보통 게임 프로그래밍이라는 것은 그래픽스 프로그래밍을 쉽게 떠올리고, 시중에
관련 책이나 학원들이 있습니다. 그러나 네트웍 프로그래밍에 관한 책이나 학원은 별로 없습니다(특히 전문 학원은 없는 것으로 알고 있습니다).
온라인 게임에서의
네트웍 프로그래밍을 한다라는 것은 대용량 네트웍 프로그램을 만들어야 한다는 것입니다. 그래서 온라인
게임 서버 프로그램을 만들려면 시스템 프로그래밍에 관한 지식과 네트웍 프로그래밍에 대한 경험이 필요합니다.
이런 이유 때문에 서버 프로그램을 만든다는 것은 쉽지 않습니다. 그러나
서버 프로그램을 쉽게 만들 수 있도록 도와주는 라이브러리를 사용할 수 있다면 어려움은 한층 작아질 것입니다.
위 코드 중 #pragma unmanaged 지시어 이하는 컴파일러에서
비관리코드로 취급합니다. 그리고 #pragma managed 이하는
관리코드로 취급합니다. 내용이 간단하고 어려운 부분이 없기 때문에 따로 자세한 설명은 생략하겠습니다.
C++/CLI는 단순하게 닷넷 플랫폼에서 사용할 수 있는 C++ 언어라기 보다는 C++ 언어의 부족한 부분을 진화 시킨 언어라고도
생각할 수 있는 부분이 꽤 있습니다. 그러나 C++/CLI는 C++과 C#의 중간의 애매한
위치에 있어서 양쪽 프로그래머 모두에게 별로 호응을 받지 못하는 것 같습니다. 그래서 C++/CLI 관련 글을 제가 처음에 생각했던 것보다는 조금 일찍 끝낼려고 합니다.
C++/CLI를 사용하는 대부분의 프로그래머들은 아마 기존의
비관리 코드를 관리코드에서 사용하고 싶을 때라고 생각합니다. C++/CLI의 기능 소개는 이번으로 일단 끝내고 앞으로는 비관리코드와의 연계에 대해서 실제 사례 보여주면서 설명하려고 합니다.
사례는 오픈 소스 네트워크 라이브러리인 HalfNetwork를
C++/CLI를 사용하여 관리코드에서 사용할 수 있도록 wrapping한
후 이것을 관리코드에서 사용할 예정입니다.
HalfNetwork는 온라인 게임 서버 프로그래머인 임영기님이
만든 것으로 ACE 라는 오픈 소스 네트워크 라이브러리를 사용하기 편하게 만든 라이브러리입니다.
C++/CLI은 네이티브
C++과 다르게 자료구조 배열을 사용하기 위해서는 array 컨테이너를 사용합니다.
array 컨테이너는 기본적으로
non-CLI 오브젝트는 사용할 수가 없습니다. 그러나non-CLI 오브젝트가 포인터라면 사용할 수 있습니다.
아래는 array 컨테이너에 non-CLI 오브젝트를 사용한 경우입니다.
using namespace System;
class CNative
{
public:
CNative()
{
Console::WriteLine(__FUNCTION__);
}
~CNative()
{
Console::WriteLine(__FUNCTION__);
}
};
int main(array<System::String ^>
^args)
{
array<CNative>^
arr = gcnew array<CNative>(2);
return 0;
}
빌드하면 위에 이야기 했듯이 아래와 같은 빌드 에러가 발생합니다.
그럼 이번에는 non-CLI 오브젝트의 포인터를 사용해 보겠습니다.
#include "stdafx.h"
#include <iostream>
using namespace System;
class CNative
{
public:
CNative()
{
Console::WriteLine(__FUNCTION__);
}
~CNative()
{
Console::WriteLine(__FUNCTION__);
}
};
int main(array<System::String ^>
^args)
{
array<CNative*>^
arr = gcnew array<CNative*>(2);
for(int
i=0; i<arr->Length; i++)
{
arr[i]
= new CNative();
}
getchar();
return 0;
}
이번에는 빌드에 문제가 없어서 아래와 같이 실행결과가 나옵니다.
그러나 위의 실행 결과를 보면 이상한 점을 발견하실 수 있을 것입니다. 그것은 CNative 오브젝트의 생성자는 호출하지만 파괴자는 호출되지 않은 것입니다. 이것은 array 컨테이너는 CLI 객체이므로 GC에서 관리하지만 non-CLI 오브젝트를 포인터 타입으로 사용한 것은 GC에서 관리하지 않으므로 만약 array가 GC에서
소멸 되기 전에 array에 담겨있는 non-CLI 오브젝트를
메모리(new로 할당한)를 해제하지 않으면 메모리 해제가 되지 않아서 메모리 릭이 발생합니다.
그래서 아래와 같이 array가 GC에서 소멸되기 전에 메모리를 해제하도록 해야합니다.
#include "stdafx.h"
#include <iostream>
using namespace System;
class CNative
{
public:
CNative()
{
Console::WriteLine(__FUNCTION__);
}
~CNative()
{
Console::WriteLine(__FUNCTION__);
}
};
int main(array<System::String ^>
^args)
{
array<CNative*>^
arr = gcnew array<CNative*>(2);
for(int
i=0; i<arr->Length; i++)
{
arr[i]
= new CNative();
}
for(int
i=0; i<arr->Length; i++)
{
delete
arr[i];
}
getchar();
return 0;
}
실행 결과
이번에는 CNative의 파괴자가 제대로 호출되고 있습니다.
출처
도서 "C++/CLI In Action"
C++/CLI를 공부하시는 분들은 "C++/CLI In Action" 책을 꼭 한번 보시기를 추천합니다.
static 생성자는 클래스의 생성자에서 static 멤버를
초기화 하고 싶을 때 사용합니다.
ref
class, value class, interface에서 사용할
수 있습니다.
#include
"stdafx.h"
#include
<iostream>
using
namespace System;
ref class
A {
public:
static int a_;
static A()
{
a_ += 10;
}
};
ref class
B {
public:
static int b_;
static B()
{
//a_ += 10; // error
b_ += 10;
}
};
ref class
C {
public:
static int c_ = 100;
static C()
{
c_ = 10;
}
};
int
main()
{
Console::WriteLine(A::a_);
A::A();
Console::WriteLine(A::a_);
Console::WriteLine(B::b_);
Console::WriteLine(C::c_);
getchar();
return 0;
}
< 결과 >
static 생성자는 런타임에서 호출하기 때문에 클래스 A의
멤버 a_는 이미 10으로 설정되어 있습니다. 그리고 이미 런타임에서 호출하였기 때문에 명시적으로 A::A()를
호출해도 실제로는 호출되지 않습니다.
클래스
B의 경우 static 생성자에서 비 static 멤버를
호출하면 에러가 발생합니다.
클래스
C의 경우 static 멤버 c_를 선언과 동시에
초기화 했지만 런타임에서 static 생성자를 호출하여 값이 10으로
설정되었습니다.
initonly
initonly로 선언된 멤버는 생성자에서만 값을 설정할 수 있습니다. 그리고 initonly static로 선언된 멤버는 static 생성자에서만 값을 설정할 수 있습니다.
ref class C
{
public:
initonly staticint x;
initonly staticint y;
initonly int z;
static C()
{
x =1;
y =2;
// z = 3; // Error
}
C()
{
// A = 2; // Error
z =3;
}
void sfunc()
{
// x = 5; // Error
// z = 5; // Error
}
};
int main()
{
System::Console::WriteLine(C::x);
System::Console::WriteLine(C::y);
C c;
System::Console::WriteLine(c.z);
return0;
}
literal
literal로 선언된 멤버는 선언과 동시에 값을 설정하고 이후 쓰기는 불가능합니다. 오직 읽기만 가능합니다.
usingnamespaceSystem;
ref class C
{
public:
literal String^ S ="Hello";
literal int I =100;
};
int main()
{
Console::WriteLine(C::S);
Console::WriteLine(C::I);
return0;
}
참고 http://cppcli.shacknet.nu/cli:static%E3%82%B3%E3%83%B3%E3%82%B9%E3%83%88%E3%83%A9%E3%82%AF%E3%82%BF http://cppcli.shacknet.nu/cli:initonly http://cppcli.shacknet.nu/cli:literal
위의 예의 델리게이트들은 모두 void를 반환하고 파라미터가 없는 것인데 당연하듯이 반환 값이나 파라미터를 가질 수 있습니다.
델리게이트의 비동기 실행
델리게이트는 비동기 실행을 지원합니다. 비동기 실행은 처리를
요청한 후 종료를 기다리지 않은 호출한 곳으로 제어를 넘겨줍니다. 델리게이트 함수가 긴 시간을 필요로
하는 작업인 경우 비동기 실행을 이용하면 프로그램의 응답성을 높일 수 있습니다.
델리게이트의 비동기 실행은 스레드를 사용합니다. 이런 경우 비동기
실행을 할 때마다 스레드의 생성과 소멸에 부담을 느낄 수도 있지만 델리게이트는 닷넷의 기능을 잘 활용하여 스레드를 생성/삭제하지 않고 스레드 풀에 있는 스레드를 사용하므로 스레드 사용에 대한 부담이 작습니다.
비동기 실행을 할 때는 주의해야 할 점이 있습니다. 비동기 실행을
하는 경우 델리게이트에는 꼭 하나의 함수만 등록해야 합니다. 만약 2개
이상 등록하였다면 예외가 발생합니다.
비동기 실행은 BeginInvoke()를 사용하고, 만약 종료를 기다리고 싶다면 EndInvoke()를 사용합니다.
#include "stdafx.h"
#include <iostream>
using namespace System;
delegate void MyDele(void);
void myfunc(void)
{
System::Threading::Thread::Sleep(3000);
}
int main(array<System::String ^>
^args)
{
MyDele^
dele = gcnew MyDele(&myfunc);
Console::WriteLine(L"1");
IAsyncResult^
result = dele->BeginInvoke(nullptr,nullptr);
Console::WriteLine(L"2");
dele->EndInvoke(result);
Console::WriteLine(L"3");
getchar();
return 0;
}
위 코드를 실행하면 '2'가 찍힌 이후 3초가 지난 이후에 3이 찍힙니다.
참고
http://cppcli.shacknet.nu/cli:delegate
http://cppcli.shacknet.nu/cli:delegate%E3%81%9D%E3%81%AE2