hiredis for Windows

Redis is simple but powerful memory-based key/value DB and it supports various programming languages. As it is like a dark horse of web, you can easily find any of its drivers for popular web programming languages. But, if you don’t write code for web and use C/C++, don’t worry! They have official C client library called hiredis also.

As you expected, of course, it works well if you are under Linux-based machine. The problem is hiredis does not support Windows compile by itself. So, you have to port its source code to Windows, huh?

Right, it’s not a good idea and not even easy for ordinary C/C++ programmer. You may think you can find some with googling. Okay, here’s my story.

I’m a game server programmer and decided to use Redis for my game. Because I had an experience with hiredis on my Mac, I didn’t expect I will spend such a long time for that – almost a whole work day.

Firstly, as other programmers usually do, I’ve googled about hiredis for windows compile and got the link. Even though the project was a little bit old, it’s under control by Microsoft! I’ve just kept following blog instruction and it worked in some wise. But, as Microsoft’s hard situation, it vomited tons of link errors I could not solve easily, while testing pub/sub functionality. I consumed most of my time to figure it out and almost gave up, almost.

At that moment, by chance, I thought about Node.js. If I google ‘hiredis windows’, the top most result is ‘hiredis – npm’. Node.js is built by C++ and I could get some clue from it. If I followed the link and there was Eldorado!

Windows

Dmitry Gorbunov (@fuwaneko) made a fork of hiredis-node with Windows support.

That fork works clearly + perfectly! No additional code is needed at all. Pub/sub works nicely, too. Node saved my life and everybody live in peace.

That’s how could I find Eldorado for a whole work day. If you encounter above situation, just ignore MSOpenTech and give your hands to Node.

WriteFile() 이 느리다구요?

새로운 프로젝트를 진행 중, 내부 망에서 진행한 테스트였음에도 반응속도가 더뎌지는 경우가 발생했다. 다행히 그 원인을 발견하는 데는 오랜 시간이 걸리지 않았는데, 다름아닌 사내 로깅(Logging) 라이브러리가 그 주범이었다. 안타깝게도, 라이브러리 담당자도 원인을 찾아주지는 못했다. 이 프로젝트는 철저히 혼자 만드는 것이었고, (존재한다면 반드시 사내 라이브러리를 사용해야 한다는 등의)기술적인 간섭 받지 않았기 때문에 로깅 라이브러리를 간단히 새로 만들었고, 결과는 만족스러웠다. 물론, 사내 라이브러리에 대한 불신은 좀 더 커졌다.

기존 프로젝트에도 꾸준히 문제되는 부분이 있었는데, 이 역시 다름아닌 로깅이었다. 이 프로젝트에서 사용되는 로깅 라이브러리는 위에서 언급했던 사내 로깅 라이브러리보다 더 오래된 것으로, 큰 문제가 없어서 그냥 사용하고 있었지만, 로그 양이 갑자기 증가하는 경우에는 문제가 발생했다. 결국, 기존의 소스코드를 약간 수정해서 성능이 개선되는 것을 확인했다.

수정한 부분은 기본 API인 WriteFile() 함수대신 fputs() 함수를 사용한 것인데, 선뜻 이해가 되지 않았던 것은, fputs() 함수도 결국 기본 API를 사용하므로, 성능이 느려지면 느려져야지, 빨리질 수 없다는 사실이었다. 그러나 분명히 성능이 개선되었으므로 소스를 커밋했다.

수정된 소스가 반영되어 세상에 나가기 이틀 전. 전체회의에서 ‘WriteFile() 함수가 fputs() 함수보다 느릴 수 없다’라는 이유로, 소스 적용이 취소되었고, 난 다시 원인을 찾기 시작했다.

내가 가장 의심했던 부분은, WriteFile()함수와 CreateFile() 함수에 붙어있는 무수한 플래그였다. 특히, WriteFile() 함수를 중심으로 살펴봤는데, 별다른 문제를 발견하지 못했다. 그렇지만, 역시나 대부분의 시간은 이 함수가 소모하고 있었다.

결국, 파일과 관련되지 않은 다른 모든 부분을 제거했다. 여전히 느렸고, 따라서 다른 부분은 문제가 아니라고 확신할 수 있었다.

그 전에, 내가 만든 로깅 라이브러리의 파일 입출력 부분을 모두 WriteFile() 함수를 사용하여 시간을 재어보니, 속도가 미미하게나마 더 빨라졌다. 당연히 이래야 하는 거잖아~!?

시간을 되돌려 다시 CreateFile() 함수와 WriteFile() 함수를 살펴보고 있었다. 그리고, 바로 전에 테스트 한 부분과의 차이가 눈에 들어왔다. 내가 만든 라이브러리에서 CreateFile() 함수를 사용할 때는 FILE_FLAG_WRITE_THROUGH를 사용하지 않았다.

다시 테스트해본 결과, 문제의 원인은 FILE_FLAG_WRITE_THROUGH이 확실했다. 구세주 MSDN과 StackOverflow의 글을 살펴본 결과 위 플래그의 역할은 다음과 같다.

C API를 사용하여 파일에 데이터를 기록하려고 할 때, 기본적으로 flush()함수를 호출하지 않으면 파일에 데이터가 기록되어있다는 것을 보장할 수 없다. 이는 쓰려는 데이터가 어딘가에 임시로 저장되어 있다는 것이며, 이로 인해 성능(속도)가 증가할 수 있다. FILE_FLAG_WRITE_THROUGH 플래그는 이 동작을 제어하며, 이 플래그를 사용하여 파일을 생성하였을 경우, WriteFile() 함수는 데이터를 버퍼링하지 않고 바로 디스크캐시로 보낸다. 그러나 디스크캐시로 보낸다는 것이 물리 디스크에 기록하는 것과 동일한 의미는 아니며, 이를 위해서는 추가로 FILE_FLAG_NO_BUFFERING 플래그를 사용해야 한다. 그러면, WriteFile() 함수의 종료 후, 데이터가 물리 디스크에 저장되었다고 보장할 수 있으며, 이에 대한 트레이드오프로 성능(속도)이 떨어지게 된다. 테스트결과, FILE_FLAG_WRITE_THROUGH 플래그만으로도 수 백배의 성능차이가 발생한다.

아마도 라이브러리 개발자는 버퍼링되는 로그가 파일에 남지 않게 될 것을 우려해서 FILE_FLAG_WRITE_THROUGH 플래그를 기본 옵션으로 설정해 둔 듯 하나, 엄밀하게는 FILE_FLAG_NO_BUFFERING 플래그도 함께 설정해야 했다.

그래도, 로깅의 성능은 상당히 크리티컬한 부분인 만큼, 그 의도는 납득하기 어려우며, 오히려 flush() 함수와 같은 인터페이스를 만드는 것이 옳지 않았을까 싶다.

결론: 네가 이해 못하는 코드는 있어도, 이해할 수 없는 현상이 발생하는 코드는 없다.

참고사이트 MSDN, StackOverflow

C++로 만든 라이브러리를 C#에서 사용하기 2

최근에 잠깐 C++/CLI에 대해서 살펴볼 일이 있어서, 무려 4년적에 적었던 ‘C++로 만든 라이브러리를 C#에서 사용하기‘를 살펴봤다.

당시와는 달리, 검색하면 관련된 내용을 찾기가 수월해진 것 같다. C++/CLI가 사용되는 경우가 늘었나 라고 생각했지만, VS2010에서 CLR프로젝트의 자동완성 기능은 제대로 동작하지 않는다. 스마트 기기류에 대응하느라 바쁜 Microsoft라 여기까지 신경 쓸 여력은 없나 보다.

일전의 설명만으론 부족한 부분을 보충하기위해 테스트코드를 만들었다.

Visual Studio 2010을 사용했으며, 솔루션의 구성은 다음과 같다.

LibAdder C++ 라이브러리
TestLibAdder LibAdder 테스트
LibAdderCS LibAdder의 인터페이스를 ManagedCode로 감싸주는 C++/CLI 라이브러리
TestLibAdderCS LibAdderCS 테스트

즉, LibAdder(C++) -> LibAdderCS(C++/CLI) -> TestLibAdderCS(C#) 이다.

예제파일은 github 에서 받을 수 있다.

참조사이트1(Managed String을 Unmanaged String으로 변환하기)
참조사이트2(델리게이트를 함수포인터로 변환하기)

비트필드의 크기(Size of Bit-fields)

union U_Data
{
    unsigned int _key;
    struct S_Data
    {
        unsigned short _code;
        unsigned short _no;
    } _evtkey;
};

개발자는 두 값(code, no)를 조합해서 고유한 키값을 손쉽게 만들기 위해 U_Data를 아래와 같이 사용하고 있다.

U_Data data;
data._evtkey._code = 10;
data._evtkey._no = 4;
unsigned int another_key = data._key; // 고유한 키값을 손쉽게(?) 얻었다!

그런데, _code의 크기가 100,000을 넘어가게 생겼다. 그래서 타입을 int로 바꿔야 했다. 이미 _key는 unsigned int 타입으로 여러 함수의 인자로 사용되고 있기 때문에, U_Data의 현 크기(4바이트)를 바꾸기는 쉽지 않다.

다행히 _no의 범위는 기껏해야 수십을 넘어가지 않았으므로, 비트필드를 사용하여, _code에 24비트를, _no에 8비트를 사용하기로 하였다.

union U_Data
{
    unsigned int _key;
    struct S_Data
    {
        int _code:24; // 타입을 바꾸고, 24비트를 할당
        unsigned short _no:8; // 8비트를 할당
    } _evtkey;
};

다른 코드는 손대지 않고, 손쉽게 _code의 크기를 늘렸다. 근데, 제대로 동작하지 않는다. 앞선 예제와 반대로, _key를 이용해서 _code와 _no를 구하려고 하니, _no에는 쓰레기 값이 있다.

이유는, 1바이트 패킹 시(#pragma pack(1)), VC++에서, U_Data의 크기를 4바이트가 아닌, 6바이트로 다루기 때문이며, 비트필드가 어떻게 메모리에 할당되는지에 대해서는 표준문서가 명시하고 있지 않다.

Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined.

– ISO/IEC 14882:2011 9.6.1

VC++에서는 비트필드가 명시된 타입에 순차적으로 값을 구겨 넣으며, 타입이 변경되면 새로운 타입에 새롭게 할당을 시작한다. 즉, S_Data의 첫번째 데이터 타입이 int이므로 4바이트에 24비트를 할당하고, 그 다음 2바이트인 unsigned short타입에 8비트를 할당해서, 총 6바이트가 사용된다. int타입인 _code에 8비트가 남았다고 해서 _no를 int에 할당하는 것이 아니란 이야기다(gcc는 4바이트를 할당한다). 명시적으로 새로운 할당을 시작하려면 비트의 크기에 0을 넣으면 된단다(http://msdn.microsoft.com/en-us/library/ewwyfdbe.aspx).

이 문제는 _code와 _no의 타입을 int로 맞춰주는 것으로 해결된다.

문제를 해결하고, 기억을 되살려보니, 정확히 2005년 10월 4일에 이와 똑같은 문제를 겪고 남겨둔 기록이 있었다.


struct T1
{
    int m_a : 5;
    int m_b : 6;
    int m_c : 5;
};
struct T2
{
    short m_a : 5;
    short m_b : 6;
    short m_c : 5;
};
struct T3
{
    char m_a : 5;
    char m_b : 6;
    char m_c : 5;
};

1로 패킹했을 경우(#pramgma pack(1)), 각 구조체의 크기는 각 각 얼마일까?

만약, sizeof()의 결과로 2를 기대했다면, T1과 T3는 잘못된 결과를 야기한다.

T1의 경우, sizeof()의 결과는 4이다. 총 비트의 합은 16이지만, 첫 번째 멤버변수의 크기가 4이기 때문에 컴파일러는 모든 비트를 첫 번째 변수의 크기인 4바이트에 구겨 넣는다. 나머지 16비트는 사용되지 않는다.

T2의 경우는 2이다. 16비트는 short int형에 딱 맞게 들어간다. 따라서 낭비하는 비트는 없다.

T3의 경우는 3이다. 컴파일러는 char의 크기인 1바이트에 5비트를 우선 구겨 넣는다. 그 다음 6비트를 확보해야 하는데, 첫 번째 1바이트에는 3비트밖에 남은 공간이 없으므로 또 하나의 char형 크기인 1바이트에 6비트를 확보한다. 마찬가지로 그 다음 5비트는 적절한 공간이 없으므로 새로운 char형 크기인 1바이트를 또 할당한다. 그래서 총 3바이트 크기가 되며, 8비트를 사용하지 않은 채 남겨두게 된다.

대부분의 컴파일러는 이와 같은 방식으로 동작한다. 그러나 표준문서에 비트필드의 크기에 대한 언급은 없기 때문에 엄밀히 따지면 이는 컴파일러의 구현에 따라서 틀려질 수 있다. 참고로, 비트필드에 접근하는 속도는 표준타입의 멤버변수에 접근하는 시간보다 느리기 때문에, 속도를 위해서라면 바람직한 방법은 아니다.

C++ 문자열 나누기

Ruby에도 C#에도 문자열에 대한 split함수가 있다. 그러나 우리의 C++에는 그런 게 없다. 이런 하잖은 기능은 직접 구현해야 한다. 이게 C++이 멋진 이유 아니겠나.

서핑 중, 문자열을 자르는 아주 멋진 코드조각을 stackoverflow에서 발견했다.

vector<string> tokens;

copy(istream_iterator<string>(iss),
    istream_iterator<string>(),
     back_inserter<vector<string> >(tokens));

구차하게 for문을 작성하지 않아도 되는 이런 우아한 방법이 있다니. 쇼크! 내가 얼마나 STL에 무신경했는지 느꼈다.
한눈에 들어오지 않아서 Visual C++ 2005의 STL소스를 참조해서 살펴봤다.

알다시피, copy()함수는 첫 번째 인자로 들어오는 입력 이터레이터를 두 번째 인자로 들어오는 입력 이터레이터까지 순회하면서 그 값을 세 번째 인자인 출력 이터레이터의 위치에 쓴다. back_inserter클래스는 컨테이터에 값을 추가하기 위한 일종의 헬퍼이다.

여기서 내가 가장 헛갈렸던 것은, ‘첫 번째 인자와 두 번째 인자의 값을 비교함에 있어서 istream_iterator는 그 값을 어떻게 가져오며, 비교대상인 istream_iterator클래스의 기본 생성자(두 번째 인자)는 무슨 의미인가’였다.

Visual C++의 STL소스를 뒤져보면, istream_iterator클래스의 기본생성자는 문자열의 char(istream_iterator클래스 기본 템플릿 파라미터)을 가리키는 포인터 변수를 0으로 설정한다. 즉, 종료문자열을 의미한다.

istream_iterator()
: _Myistr(0)
{	
    // construct singular iterator
}

그렇다면, copy()함수는 입력 이터레이터의 값을 어떻게 가져올까? 이는 istream_iterator클래서의 _GetValue()함수를 사용하며, _GetValue()함수는 basic_istream클래서의 >>연산자를 사용한다. 결국, copy()함수는 istringstream클래스에 정의된 >>연산자를 사용하게 된다.

자못 복잡해 보일 수도 있지만, 코드는 아름답다.

여기서 한 걸음 더 나아가서 delimiter를 공백문자가 아닌 다른 걸로 바꾸고 싶었다. istringstream에서는 그러한 능력을 제공하지 않는다. 역시 C++답다. split()함수를 구현하지 않은 마당에, delimiter를 설정하는 것 따위, C++에게는 사치다. 근데, getline()함수에서는 delimiter를 설정할 수 있다. 결국, 다음과 같이 쓸 수 있다.

for (string token; getline(iss, token, 'i'); tokens.push_back(token));

어쨌든, 한 줄은 한 줄. – -;

교훈 : STL소스는 복잡하다.

실행중인 인터넷 익스플로어 제어하기

이거, 간단치 않았던 작업이다. 열려있는 인터넷 익스플로어(이하 IE)를 제어해야 했다. 하다 보니, 이런걸 요청 받기도 하고 또, 겨우겨우 구현하기도 한다. 최단로(路)인지는 모르겠지만, 어쨌든 길은 길이다.

IE::_FindWindow(const TCHAR* tszSubWindowText)는 IE의 윈도우타이틀을 검색해서 핸들을 구한다. IE::Back()함수와 IE::Navigate()함수는 모두 윈도우 핸들을 찾기 위한 윈도우 타이틀을 인자로 요구하며, 각각, IE의 뒤로 가기와 URL이동을 구현한다.

이 코드는 oleacc.lib가 필요하며, Window XP에서 테스트 되었다.

근데…, 이걸 어디에 쓰냐고?

쓸 데가 있더이다~

Critical Section Block 2

이전 포스트에서 CSBLOCK 매크로를 사용해서 좀 더 간편하게 크리티컬 섹션을 정의할 수 있도록 했었다. 이에 조금 덧붙여서, 어떤 데이터형이든지, 그에 대응하는 크리티컬 섹션 변수를 만들어 줄 수 있도록 하고 싶었다. 이 작업도중 Variadic macro to count number of arguments을 읽게 되었고, 옳타구나 하고 여러 개의 자료형을 매크로의 파라미터로 사용할 수 있도록 수정했다.

다음은 이 아이디어를 구현한 코드이다.

locker 클래스는 주소값에 대응하는 값(이 코드에서는 int형 key를 사용했지만, 윈도우의 크리티컬 섹션을 위해서는 CRITICAL_SECTION 타입의 변수가 될 것이다)을 등록/삭제/검색 한다.

class locker
{
public:
    locker()
    : _key(0)
    {}

    ~locker()
    {
        map<unsigned int, int>::iterator    itr, itrend;
        itr     = _users.begin();
        itrend  = _users.end();
        for (; itr != itrend; ++itr)
        {
            unregister_addr((void*)itr->first);
        }
    }

    int register_addr(void* address)
    {
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)address);
        if (itr != _users.end())    return itr->second;

        cout    << "register: " << hex << (unsigned int)address << endl;
        _users[(unsigned int)address]   = ++_key;

        return _key - 1;
    }

    int unregister_addr(void* address)
    {
        int key = 0;
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)address);
        if (itr == _users.end())    return key;

        key = itr->second;

        cout    << "unregister: " << hex << (unsigned int)address << endl;
        _users.erase((unsigned int)address);

        return key;
    }

    int key(void* addr)
    {
        map<unsigned int, int>::iterator    itr = _users.find((unsigned int)addr);
        if (itr == _users.end())    return 0;

        return itr->second;
    }

private:
    map<unsigned int, int>  _users;
    int             _key;
};// class locker

이 클래스에는 약간의 문제가 있는데, _users, _key 변수 역시 크리티컬 섹션이라는 점이다. 따라서 이에 대한 처리도 필요하다. 또한, 주소값에 대한 key를 동적으로 생성하는데 대한 성능상의 문제도 존재한다. 그렇지만, 어디까지나 이는 아이디어를 구현하기 위한 과정으로 판단해서 일단 패스. :)

scope_lock_args 클래스는 CSBLOCK 매크로가 여러 개의 인자를 받을 수 있도록 해준다.

class scope_lock_args
{
friend class scope_lock;
public:
    scope_lock_args(int count, ...)
    {
        va_list addresses;
        va_start(addresses, count);

        for (int i = 0; i < count; ++i)
            _addresses.insert(va_arg(addresses, void*));

        va_end(addresses);
    }

    typedef std::set<void*> data_type;
private:
    std::set<void*> _addresses;
};// class scope_lock_args

scope_lock 클래스의 생성자와 소멸자에서 크리티컬 섹션의 시작과 종료를 알린다. 각 데이터는 scoke_lock_args 클래스가 주소에 따라 정렬해서 관리하기 때문에 항상 같은 순서로 락을 걸고 풀 수 있다.

class scope_lock
{
public:
    scope_lock(scope_lock_args arg)
    : _arg(arg)
    {
        scope_lock_args::data_type::iterator    itr, itrend;
        itr     = _arg._addresses.begin();
        itrend  = _arg._addresses.end();
        for (; itr != itrend; ++itr)
        {
            _locker.register_addr(*itr);
            cout    << "scope_lock::scope_lock " << hex << *itr << endl;
            // EnterCriticalSection
        }
    }

    ~scope_lock()
    {
        scope_lock_args::data_type::reverse_iterator    itr, itrend;
        itr     = _arg._addresses.rbegin();
        itrend  = _arg._addresses.rend();
        for (; itr != itrend; ++itr)
        {
            cout    << "scope_lock::~scope_lock " << hex << *itr << endl;
            // LeaveCritialSection
        }
    }

    operator bool() const
    {
        return true;
    }

private:
    static locker   _locker;
    scope_lock_args _arg;
};// class scope_lock

locker  scope_lock::_locker;

하일라이트. CSBLOCK는 최소 하나부터 최대 세 개까지의 데이터를 인자로 받을 수 있다. 물론, 필요하면 늘릴 수 있다.

#define CSBLOCK(...)    CSBLOCK_IMPL(__VA_ARGS__, 3, 2, 1)
#define CSBLOCK_IMPL(_1, _2, _3, N, ...)    if (scope_lock __sl = scope_lock_args(N, _1, _2, _3))

이제는 CSBLOCK 매크로에 여러 개의 인자를 넣을 수 있고, 그 순서에 상관없이 항상 같은 순서로 락을 사용한다.

#include <iostream>
#include <map>
#include <set>
#include <cstdarg>

using namespace std;

int main()
{
    int k   = 0;
    int l   = 0;

    CSBLOCK(&k)
    {
        cout    << "<1>" << endl;
        CSBLOCK(&l, &k)  // 순서 상관 없음
        {
            cout    << "<2>" << endl;
        }

        CSBLOCK(&k)
        {
            cout    << "<3>" << endl;
        }

        CSBLOCK(&k, &l)  // 순서 상관 없음
        {
            cout    << "<4>" << endl;
        }
    }

    return 0;
}

성능상의 문제로 실제로 사용하기에는 많은 개선이 필요하겠지만, 꽤 매력적이지 않나? ㅋㅋㅋ
C++은 확실히, 이런 것들이 재미있는 것 같다.

직접호출, boost::bind, mem_fun의 속도비교

‘boost::bind를 맘 내키는 대로 써도 될까?’라는 의문에 각 경우의 속도를 비교해보기로 했다.

우선, 재물로 사용될 클래스를 만들고…

class test_class
{
public:
    test_class()
    : _i(10)
    {}

void    show(int i)
{
    ++_i;
}

private:
    int _i;
};// class test_class

아래는 테스트코드의 일부. TIME_DURATION은 포함되는 블록의 수행시간을 마이크로 초단위로 돌려준다.

const int   repeat_num  = 100000;
hs_int64    d;

test_class  t;

TIME_DURATION(d)
{
    for (int i = 0; i < repeat_num; ++i)
        t.show(i);
}
cout    << "direct call: " << d << endl;

TIME_DURATION(d)
{
    for (int i = 0; i < repeat_num; ++i)
        boost::bind(&test_class::show, &t, i)();
}
cout    << "boost.bind call: " << d << endl;

boost::_bi::bind_t, boost::_bi::list2, boost::arg<1> > >   t_mem_boost = boost::bind(&test_class::show, &t, _1);
TIME_DURATION(d)
{
    for (int i = 0; i < repeat_num; ++i)
        t_mem_boost(i);
}
cout    << "boost.bind call via instance: " << d << endl;

TIME_DURATION(d)
{
    for (int i = 0; i < repeat_num; ++i)
        std::mem_fun(&test_class::show)(&t, i);
}
cout    << "mem_fun call: " << d << endl;

std::mem_fun1_t  t_mem_stl   = std::mem_fun(&test_class::show);
TIME_DURATION(d)
{
    for (int i = 0; i < repeat_num; ++i)
        t_mem_stl(&t, i);
}
cout    << "mem_fun call via instance: " << d << endl;

결과

direct call: 658
boost.bind call: 9152
boost.bind call via instance: 4034
mem_fun call: 1972
mem_fun call via instance: 950

테스트를 반복해도 결과는 대동소이하다. boost::bind는 직접호출보다 10배 이상 느리다. 그나마 mem_fun이 좀 낮다.
따라서…

‘boost::bind를 맘 내키는 대로 써도 될까?’ -> 살살 쓰자

C++로 만든 라이브러리를 C#에서 사용하기

내가 만든 라이브러리가 제대로 동작하는지, 그리고, 이 라이브러리는 사용하는데 있어서 문제가 없는지를 확인하기 위해서, 이를 사용한 테스트 클라이언트를 만들기로 했다. 단, 여기에 C#을 사용하고 싶었다. C#에서 간단히 처리할 수 있는 문제를 가지고 MFC와 씨름하면서 시간을 허비하기는 싫었기 때문에.

몇 가지 방법 중에 내가 선택한 방법은 C++/CLI로 C++라이브러리를 감싼 다음 이를 C#에서 사용하는 것이다. 이 방법을 선택한 이유는 순전히, 시도해본 다른 방법이 모두 실패했기 때문이다. 방법이 없다 라기보다는 내가 잘 모르고 익숙하지 않아서 그렇다. 그렇다고 C++/CLI를 사용하는데 익숙한가 하면, 그것도 아니다. 난 C++/CLI는 정말인지 필요 없다고 생각한다. 딱 한가지만 빼고. 바로 C++라이브러리를 감싸는 것. 하여튼, 난 C++/CLI를 하나도 몰랐다. 지금도 잘 모르는 상태이기 때문에, 아래에 내가 설명하는 내용에는 다소의 오류가 있을 수도 있음을 알아주기 바란다.

C++/CLI는 C++의 문법을 이해한다. 이 말은 C++/CLI에서 Unmanaged Code를 사용할 수 있다는 의미이다. 따라서 C#이 C++/CLI의 문법을 100% 이해할 수는 없다. 기본적으로 C#은 Managed Code이기 때문이다. 따라서 C++/CLI로 C++라이브러리를 잘만 감싸면, C#에서 아무런 수고 없이 이를 사용할 수 있다. 참고로 내가 사용한 Visual Studio의 버전은 2005이다.

Visual C++의 CLR타입 Class Library 프로젝트를 생성하고, C++로 만든 라이브러리를 포함시킨다. 사실, 이것은 Unmanaged C++을 사용할 때의 라이브러리 사용과정과 전혀 다르지 않다. 이제부터 할 일은 C++라이브러리 중에서 노출할 필요가 있는 인터페이스들을 Managed C++로 작성해주기만 하면 된다. 이 글의 주요 주제 되겠다.

C++의 데이터타입은 C++/CLI에서 아래의 표와 같이 바꾼다.

Unmanaged Data Type

Managed Data Type

Ref.

class

ref class

struct

value struct

enum

enum class

std::string

String^

PtrToStringAnsi()

std::map

Dictionary^

std::vector

List^

std::list

List^

function pointer

delegate

반드시 이와 같이 할 필요는 없다. std::vector를 Array로 바꾸어도 전혀 상관없다. 목표는 C++에서 데이터를 다루듯이 C#에서 이를 다룰 수 있도록 하는 것이다.

C++/CLI의 struct는 C++과는 달리, value-type으로만 사용된다. reference-type으로 사용하기위해서는 class를 사용해야 한다.

ref class로 선언되지 않은 클래스는 GC(Garbage Collection)의 관리를 받지 않으며, Unmanaged Code이다.

C++/CLI에서의 ^는 C++의 *에 해당한다. 따라서 ^형으로 선언된 변수는 gcnew키워드로 생성해야 한다. gcnew로 할당된 메모리는 GC가 자동으로 해제하므로, 그에 대해 신경 쓸 필요 없다.

Unmanaged String은 Managed String에 직접 대입될 수 없으므로, PtrToStringAnsi()함수로 사전에 변환되어야 한다. 다음은 Managed String과 Unmanaged String사이에 사용되는 매크로이다.

C++라이브러리에서 사용되는 함수 포인터는 C++/CLI에서 적절한 델리게이트로 바뀌어야한다. GetFunctionPointerForDelegate()함수를 사용할 수도 있지만, 나의 경우는 좀 더 간단하게, Unmanaged Code로 작성한 콜백함수에서 Managed Code로 작성한 콜백함수(델리게이트)를 호출했다. 가장 까다롭게 생각했고, 실제로 시간도 많이 잡아먹은 부분이 콜백함수의 처리였지만, 의외로 싱겁게 끝나버렸다.

이렇게 만들어진 라이브러리는 C#에서 바로 사용할 수 있다. 모든 인터페이스를 Managed Code로 재 작성 해야 한다는 번거로움이 있긴 하지만, 적어도 내 생각에는 이것이 가장 무난하고 또, 만만한 방법이라고 생각된다.


2012.01.31 추가.

위의 매크로는 VS2008에서는 동작하지 않는다. 참조


2012.12.04 추가.

예제

Critical Section Block

다음은 멀티쓰레드 프로그래밍에서 부지런한 프로그래머가 흔히 사용하는 코드의 일부이다. 

알다시피 이와 같은 방법은 코드의 수정을 복잡하게 만들고, 데드락을 피하기 위해서 락의 잠금과 해제 쌍을 완벽히 유지해야 하는 부담도 가지고 있다. Exception이나 갑작스런 흐름의 변화는 이러한 부담을 가중시킨다. 결정적으로…, 귀찮다.

그래서 조금 더 게으른 프로그래머는 auto_ptr을 흉내 낸 클래스를 만들어 사용한다.

크리티컬섹션의 범위가 좁은 경우에는 – 그렇다! 보호해야 하는 것은 데이터이지 함수가 아니다 – AutoCriticalSection타입의 변수의 범위를 제한해 주어야 한다.

위 코드는, 그것을 아름답지 못하게 만드는 두 가지 문제점 있다. 첫 번째는, AutoCriticalSection의 변수 명을 사용자가 선택해야 한다는 것이고, 두 번째는 블럭이 의미하는 바를 직관적으로 알아차리기 어렵다는 것이다.

아래는 이를 해결하기위한, 좀 더 많이 게으른 프로그래머의 코드이다.

두 가지 문제점을 개선한 이 코드에는 두 가지 아이디어가 녹아있다.

첫 번째는, 가독성있는 블록을 사용하기 위해서는 기존의 문법에서 방법을 빌린 것이다. 처음에는 이를 위해서 for문을 사용했다. 그러나 loop를 한번만 돌게 하기 위해서 CriticalSectionContainer클래스에 추가적인 변수가 필요했고, 블록 내에서 break와 continue를 사용할 수 있는 어색함이 남게 된다. 사소한 문제지만, 이는 변수 하나에 해당하는 양의 메모리와 한번의 점프 오퍼레이션이 소모된다. 그래서 if문을 사용했고, if문에서 참, 거짓을 판단하기 위해서 bool 연산자 오버로딩을 사용한 것이 두 번째 아이디어다.

다음은 이를 어떻게 사용하는지 보여준다.

블록이 의미하는 바가 무엇인지 분명히 나타나며, 사용자는 새로운 변수에 대해서 고민하지 않아도 되며, 이를 위해서 희생해야 하는 성능상의 손실은 if문 내에서 발생하는 한 번의 비교뿐이다.

부라보~