Thrift IDL part 1

RPC → シリアライズ → protobuf → MessagePack と見ているうちに、IDL がやりたくなってきました。MessagePack では、Thrift IDL が使えるようです。Thrift IDL*1facebook*2 で開発されたんですねー。


多言語開発の手法の一つとして、C/C++ で書いて SWIG*3 で多言語向けのバインディングを作るってのもありますが、そもそも C/C++ で書かねぇよ他の言語で書くよっていう状況が ( Web サービスだと特に ) よくありそうなので、落とし所としては Thrift IDL くらいがちょうどいいのかもしれません*4XML-RPC*5は少し野暮な感じがしますし。これだとプラットフォーム非依存にできますし。クライアントとサーバーも各言語向けのものが入っているので動作確認というかプロトタイピングは簡単にできそうですね。


コードがあったので少し見てみました。compiler/cpp/src ディレクトリに thriftl.ll、thrifty.yy というコードがあるので、yacc*6/lex*7 が使われているっぽいですね。main() で parse() を呼び出していますし。温故知新ならぬ温新知故な気分です。MessagePack で Thrift を解釈できるのも、この辺があって作るのが簡単だったから、という理由があるのかもしれません。


深く利用するまで新しいものはよく見える、ということが多いのでちょっと使ってみました。tarball をダウンロード*8して、configure します。

$ wget http://ftp.jaist.ac.jp/pub/apache//incubator/thrift/0.5.0-incubating/thrift-0.5.0.tar.gz
$ tar xzf thrift-0.5.0.tar.gz
$ cd thrift-0.5.0
$ ./configure


ひとまず、Thrift IDL コンパイラを試したいのでそこだけビルドします。終わると、thrift というプログラムが生成されます。

$ cd compier/cpp
$ make


試しに適当な thrift ファイルをつくって食わせてみます。

struct test {
  1: required string id;
  2: optional string name;
}
$ ./thrift --gen cpp test.thrift


gen-cpp というディレクトリが生成されて、そこにファイルが四つほど生成されました。

$ ls gen-cpp
test_constants.cpp	test_types.cpp
test_constants.h	test_types.h


xxx_constants というファイルは今回定数を使っていないので無視するとして、test_types.h と test_types.cpp はこんな感じでした。

/**
 * Autogenerated by Thrift
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 */
#ifndef test_TYPES_H
#define test_TYPES_H

#include <Thrift.h>
#include <TApplicationException.h>
#include <protocol/TProtocol.h>
#include <transport/TTransport.h>





typedef struct _test__isset {
  _test__isset() : name(false) {}
  bool name;
} _test__isset;

class test {
 public:

  static const char* ascii_fingerprint; // = "5B708A954C550ECA9C1A49D3C5CAFAB9";
  static const uint8_t binary_fingerprint[16]; // = {0x5B,0x70,0x8A,0x95,0x4C,0x55,0x0E,0xCA,0x9C,0x1A,0x49,0xD3,0xC5,0xCA,0xFA,0xB9};

  test() : id(""), name("") {
  }

  virtual ~test() throw() {}

  std::string id;
  std::string name;

  _test__isset __isset;

  bool operator == (const test & rhs) const
  {
    if (!(id == rhs.id))
      return false;
    if (__isset.name != rhs.__isset.name)
      return false;
    else if (__isset.name && !(name == rhs.name))
      return false;
    return true;
  }
  bool operator != (const test &rhs) const {
    return !(*this == rhs);
  }

  bool operator < (const test & ) const;

  uint32_t read(::apache::thrift::protocol::TProtocol* iprot);
  uint32_t write(::apache::thrift::protocol::TProtocol* oprot) const;

};



#endif
/**
 * Autogenerated by Thrift
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 */
#include "test_types.h"



const char* test::ascii_fingerprint = "5B708A954C550ECA9C1A49D3C5CAFAB9";
const uint8_t test::binary_fingerprint[16] = {0x5B,0x70,0x8A,0x95,0x4C,0x55,0x0E,0xCA,0x9C,0x1A,0x49,0xD3,0xC5,0xCA,0xFA,0xB9};

uint32_t test::read(::apache::thrift::protocol::TProtocol* iprot) {

  uint32_t xfer = 0;
  std::string fname;
  ::apache::thrift::protocol::TType ftype;
  int16_t fid;

  xfer += iprot->readStructBegin(fname);

  using ::apache::thrift::protocol::TProtocolException;

  bool isset_id = false;

  while (true)
  {
    xfer += iprot->readFieldBegin(fname, ftype, fid);
    if (ftype == ::apache::thrift::protocol::T_STOP) {
      break;
    }
    switch (fid)
    {
      case 1:
        if (ftype == ::apache::thrift::protocol::T_STRING) {
          xfer += iprot->readString(this->id);
          isset_id = true;
        } else {
          xfer += iprot->skip(ftype);
        }
        break;
      case 2:
        if (ftype == ::apache::thrift::protocol::T_STRING) {
          xfer += iprot->readString(this->name);
          this->__isset.name = true;
        } else {
          xfer += iprot->skip(ftype);
        }
        break;
      default:
        xfer += iprot->skip(ftype);
        break;
    }
    xfer += iprot->readFieldEnd();
  }

  xfer += iprot->readStructEnd();

  if (!isset_id)
    throw TProtocolException(TProtocolException::INVALID_DATA);
  return xfer;
}

uint32_t test::write(::apache::thrift::protocol::TProtocol* oprot) const {
  uint32_t xfer = 0;
  xfer += oprot->writeStructBegin("test");
  xfer += oprot->writeFieldBegin("id", ::apache::thrift::protocol::T_STRING, 1);
  xfer += oprot->writeString(this->id);
  xfer += oprot->writeFieldEnd();
  if (this->__isset.name) {
    xfer += oprot->writeFieldBegin("name", ::apache::thrift::protocol::T_STRING, 2);
    xfer += oprot->writeString(this->name);
    xfer += oprot->writeFieldEnd();
  }
  xfer += oprot->writeFieldStop();
  xfer += oprot->writeStructEnd();
  return xfer;
}


コードはいかにもプログラムが生成したコードという感じがします。なるほどこんな感じなのか〜と眺めていると、フィンガープリント*9が出てきますが、利用されている形跡がありません。test クラスは他のクラスを継承しているわけでもないので、Thrift の何らかの機能を使うとこれが利用されるのかもしれません。


test::write() はシンプルですが、test::read() はそれなりに面倒なことをやってくれています。thrift ファイルで required とか optional として指定したかどうかで振る舞いが変わることが見て取れます。あと、途中で知らないフィールドが出てきたらすっ飛ばすようです。


RPC は大抵シンプルなリクエスト・レスポンスで完結するのでこれで十分な気がしますが、接続を確立したまま相互にメッセージをやりとりし、かつ複数種類のメッセージが順序が入り乱れているような場合、Thrift IDL をどうやって使うべきなんだろう。こういう場合、私はよく TLV*10 メッセージを使います。T と L は固定長で、V が可変長になっていて、T でメッセージを識別し、L で V の長さを知り、V の解釈は T 毎に用意したものにまかせる、というやりかたです。V の終端が何らかの形で定義できる場合、TV でも十分です。なんで、TL または T を struct として定義しておけば十分そうですが・・・。


とりあえず今日はここまでにしておこう。

*1:Apache Thrift

*2:オープンソース - Facebook開発者

*3:Simplified Wrapper and Interface Generator

*4:Web サービスはひとつの言語にしぼったとしても、RPC サービス、Web ブラウザ向けクライアント、Ajax クライアント、iOS App クライアント、Android クライアント・・・などを考えると、そんな気がします

*5:XML-RPC Home Page

*6:Yet Another Compiler-Compiler

*7:Lexical Analyzer Generator

*8:ダウンロードには curl を使うこともありますが、facebook といえば wget ですよね

*9:Thrift のコードを見るとこれは MD5 のようです

*10:Type-length-value