2.PIDL 파라메터 타입 다루기(마샬링)

PIDL에서는 정수형이나 문자열 등 기본적인 변수 타입 외의 다양한 타입을 사용할 수 있습니다.

참고: C# 언어의 마샬링은 C# 언어에서 사용자 정의 타입을 마샬링하기에 설명되어 있습니다.

2.1RMI에 사용자 정의 클래스 타입을 사용하기

RMI 호출을 메시지로 변환하거나 메시지로부터 RMI를 호출하기 위한 값을 추출하는 것을 마샬링(marshaling)이라고 칭합니다.

ProudNet에서는 int나 float 등 기본적인 타입에 대한 마샬링 기능을 제공하고 있습니다.

물론, 이러한 기본적 타입 뿐만 아니라 사용자가 정의한 클래스 타입을 사용할 수도 있습니다. 그러기 위해서는 다음 함수들을 오버로딩으로 구현해야 합니다.

// MyType.h

namespace Proud
{
    // 호출한 RMI 함수의 내용을 문자열로 만들어 출력한다.
    // 로그를 만들 때 유용하다.
    void AppendTextOut(String &a,const MyType &b);

    // 메시지 버퍼로부터 커스텀 타입의 내용을 읽어온다.
    CMessage& operator>>(CMessage &a, MyType &b);

    // 메시지 버퍼로 커스텀 타입의 내용을 넣는다.
    CMessage& operator<<(CMessage &a, const MyType &b);
}

ProudNet의 RMI 기능 내부에서는 Proud.CMessage를 사용하고 있습니다. 그리고 위 함수들의 오버로딩을 통해 RMI의 파라메터가 마샬링됩니다.

여기서 Proud.CMessage가 사용되고 있습니다. Proud.CMessage는 ProudNet에서 RMI를 메시지로 바꾸거나 메시지로부터 파라메터를 읽어올 때 사용하는 메시지 데이터를 갖고 있으며, 스트림 객체로서 사용됩니다.

marshaling을 하는 함수에 스트림을 구현하는 예는 다음과 같습니다.

// MyType.cpp

namespace Proud
{
    CMessage& operator>>(CMessage &a, MyType &b)
    {
        a>>b.x,b.y>>b.z>>b.w;
        return a;
    }
    CMessage& operator<<(CMessage &a, const MyType &b)
    {
        // a.UseInternalBuffer()를 사용하지 말 것!
        a<<b.x,b.y<<b.z<<b.w;
        return a;
    }
    void AppendTextOut(String &a,const MyType &b)
    {
        String f;
        f.Format(L"{x=%f,y=%f,z=%f,w=%f}",b.x,b.y,b.z,b.w);
        a+=f;
    }
}

마지막으로, PIDL compiler에서 만들어진 proxy, stub 파일들을 include를 하기 전에 위 오버로딩된 메서드들이 선언된 헤더 파일이 먼저 include되어야 합니다.

// 예1
#include "MyType.h"
#include "MyPIDL_proxy.h"

// 예2
#include "MyType.h"
#include "MyPIDL_stub.h"

이것이 실제로 구현된 예제는 11. 커스텀 타입 객체의 마샬링 예제 프로그램 및 <Sample/CasualGame/GCServer/FarmCommon.h> 를 참고하십시오.

더불어, 직접 구현하신 마샬링 기능이 잘 작동함을 확인하시려면 Proud.TestMarshal() 을 사용하시는 것도 좋습니다.

2.2조건에 따라 마샬링 방법을 달리 하기

예를 들어, 캐릭터의 타입간 각 필드가 유효하거나 무효 여부가 서로 다른 캐릭터의 정보를 RMI 파라메터에서 마샬링하는 방법이 있습니다. switch/case 문이나 객체의 다형성을 이용해서 다방법 마샬링을 구현할 수 있습니다. 쉬운 이해 도모를 위해, swich/case를 쓰는 예시를 보여드리겠습니다. 아래는 그 예입니다.

namespace Proud
{
    enum UnitType
    {
        Zergling,     // 스타크래프트의 저글링(지상형 통상 공격 유닛)
        Queen,        // 스타크래프트의 퀸(비행형 특수 기술 사용 유닛)
        Broodling    // 퀸이 브루들링 스킬을 써서 생성된, 생존 시간이 짧은 공격 유닛
    };

    struct Unit
    {
        UnitType m_type;          // 유닛의 타입
        Vector2D m_position;    // 유닛의 위치
        int m_energy;            // 유닛의 보유 에너지(혹은 마나) -> 퀸만 유효
        float m_lifeTime;        // 유닛의 생존 시간 시한 -> 브루들링만 유효
        int m_attackPower;        // 유닛의 공격력->저글링만 유효
    };

    CMessage& operator<<(CMessage& msg,const Unit& unit)
    {
        msg<<unit.m_type<<unit.m_position;
        switch(unit.m_type)
        {
        case Zergling:
            msg<<unit.m_attackPower;
            break;
        case Queen:
            msg<<unit.m_energy;
            break;
        case Broodling:
            msg<<unit.m_lifeTime;
            break;
        }
        return msg;
    }

    CMessage& operator>>(CMessage& msg,Unit& unit)
    {
        msg>>unit.m_type>>unit.m_position;
        switch(unit.m_type)
        {
        case Zergling:
            msg>>unit.m_attackPower;
            break;
        case Queen:
            msg>>unit.m_energy;
            break;
        case Broodling:
            msg>>unit.m_lifeTime;
            break;
        }
        return msg;
    }
}

2.3Bit 단위의 데이터를 마샬링하기

를 할 때, 메시지에 저장되는 데이터의 용량을 줄이기 위해 비트 단위로 데이터를 마샬링할 수 있습니다.

Proud.CMessage는 bit 단위로 데이터를 저장하는 다음 메서드들이 있습니다.

•bit 단위 읽기: Proud.CMessage.ReadBits

•bit 단위 쓰기: Proud.CMessage.WriteBits

아래는 사용 예입니다.

namespace Proud
{
    struct MyType
    {
        int x,y,z;
    };
    CMessage& operator>>(CMessage &a, MyType &b)
    {
        // x 값을 읽어들이는데 6비트 사용
        a.ReadBits(b.x,6);
        // y 값을 읽어들이는데 3비트 사용
        a.ReadBits(b.y,3);
        // z 값을 읽어들이는데 15비트 사용. 즉 도합 6+3+15=24bit(3 byte)사용
        a.ReadBits(b.z,15);
        return a;
    }
    CMessage& operator<<(CMessage &a, const MyType &b)
    {
        a.WriteBits(b.x,6); // x,y,z값을 비트 단위로 저장한다.
        a.WriteBits(b.y,3);
        a.WriteBits(b.z,15);
        return a;
    }
}

Bit 단위 데이터 마샬링의 주의 사항

기록하려는 비트 갯수가 기록하려는 실제 값의 범위를 벗어나서는 안됩니다. 예를 들어 int를 기록하려고 하는데 정작 int 안에 들어가 있는 값이 음수인 경우 첫 bit가 1입니다. 이럴 때 bit 양을 줄이기 위해 31비트 이하를 기록하려고 하는 경우 첫 bit의 값이 누락됩니다. 따라서 비트 단위 읽기/쓰기를 할 때에는 이 점을 주의해야 합니다.

2.4enum 타입을 마샬링하기

enum 타입을 마샬링하려면 아래와 같은 함수들을 구현하면 됩니다.

enum type { ... } ;

namespace Proud
{
    inline CMessage& operator<<(CMessage& a,type b)
    {
        a<<(int)b;
        return a;
    }
    inline CMessage& operator>>(CMessage& a,type& b)
    {
        int x;
        a>>x;
        b=(type)x;
        return a;
    }
    inline void AppendTextOut(String &a,type b)
    {
        String txt;
        txt.Format(L"%d",(int)b);
        a+=txt;
    }
}

ProudNet에 이미 정의된 매크로 PROUDNET_SERIALIZE_ENUM 를 이용하면 위 구현을 아래와 같이 쉽게 할 수 있습니다.

PROUDNET_SERIALIZE_ENUM(type)

2.5콜렉션(배열 등)을 마샬링하기

ProudNet에서는 몇 가지 기본 콜렉션(배열 등) 타입에 대한 마샬링을 바로 할 수 있도록 준비되어 있습니다. Proud.CFastArray, Proud.CFastMap, std.vector , CAtlArray 를 지원합니다. 보다 자세한 것은 marshaler.h에서 operator>>, operator<< 오버라이드를 참고하십시오.

아래는 배열 타입을 PIDL에서 선언한 예입니다.

Foo([in] Proud::CFastArray<MyType> a, [in] std::vector<MyType> b);

만약 marshaler.h에서 기본 정의된 콜렉션 타입 외의 마샬링이 필요한 경우에는 필요로 하는 콜렉션에 대응하는 오버라이드를 구현해야 합니다. 이를 위해 2.1 RMI에 사용자 정의 클래스 타입을 사용하기 를 참고하십시오. 이미 구현된 선례를 marshaler.h에서 참고하시는 것도 권장합니다.

다음은 std.vector를 마샬링하는 예입니다. ProudNet에서 아직 지원하지 않는 콜렉션 타입에 대해서 마샬링을 하고자 할 때는 다음 루틴을 참고해서 마샬링하는 루틴을 작성하시면 됩니다.

namespace Proud
{
    // vector에서 사용할 수 있는 serialization functions
    // for output to stream
    template<typename elem>
    inline CMessage& operator>>(CMessage &a, std::vector<elem> &b)
    {
        // 크기를 얻는다.
        int size;
        a >> size;

        // 크기가 받아들일 수 없는 경우에는 예외를 발생시킨다.
        // 해킹된 경우 등을 의심할 수 있다.
        if (size<0 ||size >= CNetConfig::MessageMaxLength)
            ThrowExceptionOnReadArray(size);

        // 메모리 frag를 줄이기 위해
        b.reserve(size);
        b.resize(0);

        // 배열 항목 하나 하나를 읽는다.
        elem e;
        for (int i = 0;i < size;i++)
        {
            a >> e;
            b.push_back(e);
        }
        return a;
    }

    // vector, list등 unary item elem 등에서 
    // 사용할 수 있는 serialization functions
    // for input from stream
    template<typename elem>
    inline CMessage& operator<<(CMessage &a, const std::vector<elem> &b)
    {
        // 배열 크기를 기록한다.
        int size = (int)b.size();
        a << size;
        
        // 각 배열 인자를 기록한다.
        for (std::vector<elem>::const_iterator i = b.begin();i != b.end();i++)
        {
            a << (*i);
        }
        return a;
    }

    template<typename elem>
    inline void AppendTextOut(String &a, std::vector<elem> &b)
    {
        a += L"<vector>";
    }
}