본문 바로가기

도구의발견

[idlc] 객체 직렬화(Object Serialize) 컴파일러

idlc란?

IDLC란 인터페이스 정의 언어(Interface Definition Language) Compiler의 약자로써, 구글의 프로토콜버퍼, 플랫버퍼, 아파치의 스리프트, 넷플릭스에서 사용 되는 gRPC 처럼 IDL로 정의된 구조화된 데이터를 직렬화(serialize) 하는 코드를 다양한 프로그래밍 언어로 생성하는 컴파일러 입니다.

 

지원 언어 :

  • C++ : #멀티플랫폼 지원, #std 11 버전 이상 필요, #header only 라이브러리
  • C#
  • Python : Python 2.x 버전 까지 지원(3.x 이상 버전 미지원)

idlc의 장점

1. 분산 컴퓨팅 환경에서 구조화된 객체를 전달하기에 좋습니다.

서버는 C++로 개발 되고, 클라이언트는 C#으로 개발 되었을 때, idlc를 사용하면 각각의 언어로 직렬화 코드를 개발 할 필요 없이 IDL로 데이터를 정의하고 공유하는 것만으로 각 언어별 직렬화 코드를 생성 할 수 있습니다.

 

2. 익숙합니다.

기존 C++/C# 문법과 거의 동일하여 기존 C++/C# 사용자의 추가적인 교육 비용이 적습니다.

구글 프로토콜버퍼 처럼 접근 인터페이스를 만들거나 네이밍을 변경하지 않아 일반 네이티브 자료 구조와 동일하게 사용 할 수 있습니다.

 

3. 간단 합니다.

idlc는 추가적인 dll, lib를 필요로 하지 않습니다. 여러분이 작성한 코드를 프로젝트에 포함시키는 것과 똑같이 프로젝트에 추가만 해주시면 언어와 상관 없이 동일한 객체 직렬화 기능을 사용 하실 수 있습니다.

 

4. 기존 프로젝트에 적용이 용이 합니다.

gRPC나 thrift 처럼 stub을 생성하지 않고 데이터 시리얼라이즈만을 담당하기 때문에 기존 프로젝트에 적용하기 원활합니다.

 

5. 성능과 안정성이 검증 됐습니다.

상용화 프로젝트의 서비스 적용을 완료 했습니다.

idlc를 사용한 상용화 프로젝트(마기아 카르마 사가, 삼국전투기 모바일)

 

다운로드

  • Github : github.com/ChoiIngon/gamnet/tree/master/idlc
  • 윈도우 바이너리 : Download idlc.exe
  • 리눅스 바이너리 : 안타깝게도 리눅스는 다양한 운영체제별로 바이너리를 모두 제공할 수 없어 바이너리를 직접 제공하지 않습니다. github에서 프로젝트 다운로드 후 cmake를 통해 직접 빌드 하셔야 합니다.

사용법

idlc의 사용법을 간단히 요약하면 'idl 객체 정의 -> idlc 컴파일 -> 프로젝트에 포함'. 이렇게 세가지 단계를 거쳐야 합니다. 쉬운 이해를 위해 아래 예제를 살펴 보며 idlc의 사용법을 알아 보도록 하겠습니다.

1. 시리얼라이즈 객체 정의

스칼라 타입 객체 정의

각 기본 자료형의 이름이 약간 다른것 빼고는 C++, C#의 문법과 동일하기 때문에 C++, C#에 익숙하신 분들이라면 idl을 이용해 객체를 정의하는 방식이 매우 익숙 할 것 입니다. 아래와 같이 hello_world.idl 이라는 파일을 만들고 HelloWorld라는 객체를 정의 해보겠습니다.

// hello_world.idl

struct HelloWorld
{
    byte              byte_;
    boolean           boolean_;
    int16             int16_;
    int32             int32_;
    int64             int64_;
    uint16            uint16_;
    uint32            uint32_;
    uint64            uint64_;
    float             float_;
    double            double_;
    string            string_;
    list<int32>       list_;
    array<int32>      dynamic_array_;
    array<int32, 10>  static_array_;
    map<int32, int32> map_;
};

참고 : idlc 에서 사용가능한 데이터 타입 키워드

더보기
keyword in C++ in C# in Python serialized data size
byte char sbyte - 1 byte
boolean bool bool - 1 byte
int16 int16_t short - 2 bytes
int32 int32_t int - 4 bytes
int64 int64_t long - 8 bytes
uint16 uint16_t ushort - 2 bytes
uint32 uint32_t uint - 4 bytes
uint64 uint64_t ulong - 8 bytes
float float float - 4 bytes
double double double - 8 bytes
string std::string string - 4 + character count bytes
list<T> std::list<T> List<T> [] 4 + sizeof(T) * element count bytes
array<T> std::vector<T> List<T> [] 4 + sizeof(T) * element count bytes
array<T, N> T array[N] int[10] [] * 10 sizeof(T) * element count bytes
map<T, V> std::map<T, V> Dictionary<T, V> {} 4 + (sizeof(T) + sizeof(N)) * element count bytes

※ C++의 포인터 타입은 다른 언어들과의 호환성 문제로 인해 지원되지 않습니다.

Enum & User Define Struct

idlc에서는 기본 데이터 타입 뿐아니라 enum 및 사용자 정의 데이터 타입도 지원 합니다. 일반 클래스를 정의하듯 struct 내부에 다른 사용자 정의 데이터 타입을 멤버로 갖는 것도 가능 합니다.

enum ErrorCode 
{
    Success                     = 0,
    MessageFormatError          = 1000,
    MessageSeqOmmitError        = 1001,
    InvalidUserError            = 2000,
    Max
};

struct UserData 
{
	string user_id;
	uint32 user_seq;
	uint32 frame;
};

struct MsgCliSvr_Login_Req
{
	string user_id;
};

struct MsgSvrCli_Login_Ans
{
	ErrorCode error_code;
	UserData user_data;
};

주석

idlc의 주석은 C++/C#의 그것과 동일 합니다. 한 줄 주석 처리를 위한 // 과 여려 줄을 동시에 주석 처리 하기 위한 /* */를 지원 합니다.

// 한줄 주석

/* 여러 줄 주석
struct DeprecatedHelloWorld
{
};
지금은 사용하지 않지만 혹시나 불안한 마음에 남겨 놓는 옛날 코드
*/
struct HelloWorld // 여기도 쓸 수 있음
{
};

리터럴 블록

[.cpp|.cs|.py] %% ~ %% 와 같이 특정 언어의 확장자로 시작해 %% 로 감싸진 블록은 idl에서 컴파일 하지 않고 지정된 언어로 컴파일 할 때만 있는 그대로를 결과 파일에 출력 합니다. 

// -lcpp 옵션일 경우만 결과 파일에 출력 됩니다.
.cpp %%
#include "MessageCommon.h"
namespace Message { 
%%

// -lcs 옵션일 경우만 결과 파일에 출력 됩니다.
.cs %%
namespace Message {
%%

// 공통으로 출력 됩니다
struct HelloWorld
{
};

.cpp %%
}}
%%
.cs %%
}}
%%

상속

상속은 파이썬 상속 문법과 비슷합니다. struct ObjectName(ParentObjectName) 과 같이 괄호 안에 상속 받고자 하는 객체 정의의 이름을 써주면 됩니다.

enum EquipItemPartType
{
    Invalid,
    Cloak,
    Legs,
    Body,
    Boots,
    Head,
    Gloves,
    LeftHand,
    RightHand,
    Max
};

struct ItemData
{
    uint64 item_seq;
    uint32 item_index;
    int32 item_count;
};

struct EquipItemData(ItemData) // 상속
{
    EquipItemPartType part_type;
};

2. 컴파일

idl 작성을 완료 하였으면 이제 해당 파일을 컴파일 하는 과정이 필요 합니다. 기본 사용법은 커맨드 창에서 아래와 같이 명령어를 입력하면 -l 옵션의 인자에 따라 C++, C#, Python 언어로 변환된 같은 이름의 확장자가 다른 파일이 생성 됩니다.

idlc -l [cpp|cs|py] <input file>.idl

※ 리눅스에서는 idlc.exe를 ./idlc 와 같이 실행 하셔야 합니다.

C++

> idlc -lcpp hello_world.idl
in-file:hello_world.idl
outfile:hello_world.h
complete compile(hello_world.idl -> hello_world.h)
※ C++ 경우 생성 되는 결과 파일은 플랫폼 호환성을 가지며(window, linux, mac 지원), 헤더 파일 인클루드만으로 모든 기능을 사용 할 수 있습니다.

C#

> idlc -lcs hello_world.idl
in-file:hello_world.idl
outfile:hello_world.cs
complete compile(hello_world.idl -> hello_world.cs)

Python

> idlc -lpy hello_world.idl
in-file:hello_world.idl
outfile:hello_world.py
complete compile(hello_world.idl -> hello_world.py)

 

위 과정을 거치고 난 후 hello_world.idl이 있는 위치에 hello_world.h 또는 .cs, .py 파일이 생성 되어 있는 것을 확인 할  수 있습니다. 이제 이 결과파일을 이용해 다음장 부터 본격적인 시리얼라이즈/디시리얼라이즈를 해보도록 하겠습니다.

3. 시리얼라이즈/디시리얼라이즈

이번장에서는 idlc로 부터 정의된 객체를 이용하여 어떻게 시리얼라이즈/디시리얼라이즈를 하는지 각 언어별 코드 샘플과 함께 살펴 보도록 하겠습니다.

 

idlc를 이용해 정의된 객체는 시리얼라이즈/디시리얼라이즈를 위해 아래 세개 멤버 함수를 가집니다.

  • 객체를 바이트스트림으로 시리얼라이즈 하기 위한 Store()
  • 바이트스트림에서 객체로 디시리얼라이즈 하기 위한 Load()
  • 객체의 현재 사이즈를 리턴하는 Size()

C++

#include <iostream>
#include "hello_world.h" // idlc에 의해 생성된 헤더 파일 인클루드

int main()
{
    std::vector<char> byte_stream; // 시리얼라이즈에 사용될 바이트스트림

    HelloWorld h1; // hello_world.idl에서 정의한 HelloWorld 자료구조
    h1.int32_ = 2021;
    h1.string_ = "Hello World";
    h1.Store(byte_stream); // byte_stream 버퍼에 객체의 데이터를 시리얼라이즈

    HelloWorld h2;
    h2.int32_ = 0;
    h2.string_ = "";
    h2.Load(byte_stream); // byte_stream 버퍼로 부터 디시리얼라이즈

    std::cout << h2.string_ << " " << h2.int32_ << std::endl;
}

Store() 함수를 이용해 객체 h1의 내용을 byte_stream에 시리얼라이즈하고 다시 h2 객체로 디시리얼라이즈 했습니다. 테스트를 위해 h2 객체의 int32_와 string_을 0과 "" 초기화 했음에도 불구하고 Load() 함수 호출 이후 h1으로 부터의 byte_stream이 h2객체에 잘 디시리얼라이즈 되었음을 알 수 있습니다.

C#

using System;

namespace hellworld_cs
{
    class Program
    {
        static void Main(string[] args)
        {
            System.IO.MemoryStream byte_stream = new System.IO.MemoryStream();
            HelloWorld h1 = new HelloWorld();
            h1.int32_ = 2021;
            h1.string_ = "HelloWorld";
            h1.Store(byte_stream);
            
            byte_stream.Seek(0, System.IO.SeekOrigin.Begin);
            
            HelloWorld h2 = new HelloWorld();
            h2.int32_ = 0;
            h2.string_ = "";
            h2.Load(byte_stream);
            
            Console.WriteLine("{0} {1}", h2.string_, h2.int32_);
        }
    }
}

C++ 예제에서는 std::vector<char>을 바이트스트림으로 이용한 반면, C#에서는 MemoryStream 클래스를 시리얼라이즈를 위한 바이트스트림으로 이용하고 있습니다.

byte_stream.Seek(0, System.IO.SeekOrigin.Begin);
MemoryStream은 write를 하거나 read 하는 만큼 read 포인터를 증가 시키기 때문에 스트림을 재사용하기 위해서는 Seek()함수를 이용하여 read 포인트를 항상 맨 앞으로 이동 시켜 줘야만 합니다.

Python

# -*- coding:utf8 -*-
from hello_world import *

h1 = HelloWorld()
h1.int32_ = 2021
h1.string_ = "Hello World"
byte_stream = h1.Store()

h2 = HelloWorld()
h2.int32_ = 0
h2.string_ = ""
h2.Load(byte_stream)

print(h2.string_ + " " + str(h2.int32_))
※ Python 2.x 버전 까지 지원 됩니다. 3.x 버전에서 실행하면 코드 호환성 문제로 에러가 발생 합니다.
혹시 여러분 중에 idlc를 좋게 보시고 사용하시고자 하시는 분은 얼마든지 사용하셔도 좋습니다(무료 입니다!!)
그리고 적용하시다 어려운 부분, 의문이 드는 부분, 추가 개선이 이루어지면 좋겠다고 생각이 드시는 부분에 대해서 의견을 남기고 싶으신 분들은 이 포스트의 댓글로 남겨 주시면 제가 종종 와서 확인하고 답변 드리도록 하겠습니다.
감사합니다.

부록 1. Tip : Visual Studio Custom Compile

매번 idl 변경이 발생 할 때 마다 커맨드 창에서 idl을 컴파일하고 프로젝트로 돌아와 다시 빌드를 하는 것은 여간 귀찮은 일이 아닙니다. 하지만 비주얼 스튜디오의 커스텀 컴파일 기능을 사용하면 빌드시 idl을 같이 컴파일 할 수 있습니다.

 

"파일 속성 > 일반 > 항목 형식 > 사용자 빌드 도구 선택 > 확인" 사용자 커스텀 빌드 도구를 사용 할 수 있도록 설정 하고 창을 닫은 후, 다시 "파일 속성 > 사용자 지정 빌드 도구" 를 선택하여 아래와 같이 입력해줍니다.

  • 명령줄 : $IDLC_EXE\PATH\idlc.exe -lcpp $IDL_FILE\PATH\%(Filename).idl
  • 설명 : idl build : $IDLC_EXE\PATH\idlc.exe -lcpp $IDL_FILE\PATH\%(Filename).idl
  • 출력 : %(Filename).h

$IDLC_EXE\PATH $IDL_FILE\PATH 부분은 각자의 환경에 맞는 상대 경로 혹은 절대 경로를 적어 주시면 됩니다.

부록 2. 참고

idlc 깃허브 프로젝트 : github.com/ChoiIngon/gamnet/tree/master/idlc

사용 예제 프로젝트 : example/BasicStandAloneServer

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!