2011年4月16日土曜日

Go fix that **** code...

Goのユーザーとしてちょっと辟易していたこととして、開発チームがGoそのものの文法や標準ライブラリのAPIをガツガツ変更することが挙げられる。いやホントに良く変わる。発表されてからもう1年経ったけど、まだ当分安定するようには見えない。

で変更されて何が困るかというと、古いコードのコンパイルが通らなくなること。自分が書いたコードならいざしらず、他人が書いたコードのコンパイルが通らなくなってしまうと結構厄介。メンテナー次第では下手すると当分コードがアップデートされずにそのままなんてこともありうる。じゃあ自分で直そうにも、もしかしたらエンバグするかもしれないし、そもそもが面倒だからあんまりヤル気にならない。そうすると自分が使ってたパッケージが使えなくなって、でもかといって古いバージョンのGoを使う気にもなれなくて、開発に対するモチベーションが下がって…という酷い悪循環におちいる。自分は。

Gofix

そういう状況を開発チームも理解していたのか、ちょっと前にGofixなんていうツールが発表された。詳しくはRussの書いたブログを読んでもらうとして、Gofixがやってくれるのは簡単に言うと「コードの自動修復」だ。どうやら古いAPIやら文法が使われているコードを自動的に修復して、ナウいバージョンに仕立てなおしてくれるらしい。

Go fix Thrift

ということで、実益と実験も兼ねてThriftのGoコードをGofixにかける実験をやってみた。なお使用したバージョンはGoがr8131、Thriftがr1093950。

Thrift修正前

Thriftの修正前ライブラリを普通にコンパイルしようとしたら、以下のようになる。net.DialあたりのAPIが変わったせいでコンパイルが通らない。

$ make
cd thrift/ && gomake install
make[1]: Entering directory `/home/masato/lib/thrift/lib/go/thrift'
8g -o _go_.8 tapplication_exception.go tbase.go tbinary_protocol.go tcompact_protocol.go tcompare.go tcontainer.go texception.go tfield.go tframed_transport.go thttp_client.go tiostream_transport.go tlist.go tjson_protocol.go tmap.go tmemory_buffer.go tmessage.go tmessagetype.go tnonblocking_server.go tnonblocking_server_socket.go tnonblocking_socket.go tnonblocking_transport.go tnumeric.go tprocessor.go tprocessor_factory.go tprotocol.go tprotocol_exception.go tprotocol_factory.go tserver.go tserver_socket.go tserver_transport.go tset.go tsimple_server.go tsimple_json_protocol.go tsocket.go tstruct.go ttransport.go ttransport_exception.go ttransport_factory.go ttype.go
tnonblocking_socket.go:111: too many arguments in call to net.Dial
tsocket.go:137: too many arguments in call to net.Dial
make[1]: *** [_go_.8] Error 1
make[1]: Leaving directory `/home/masato/lib/thrift/lib/go/thrift'
make: *** [thrift/.install] Error 2

Thrift修正後

この状況から、まずGofixを走らせた上でmakeすると…

$ gofix .
thrift/tnonblocking_socket.go: fixed netdial
thrift/tsocket.go: fixed netdial
$ make
cd thrift/ && gomake install
make[1]: Entering directory `/home/masato/lib/thrift/lib/go/thrift'
8g -o _go_.8 tapplication_exception.go tbase.go tbinary_protocol.go tcompact_protocol.go tcompare.go tcontainer.go texception.go tfield.go tframed_transport.go thttp_client.go tiostream_transport.go tlist.go tjson_protocol.go tmap.go tmemory_buffer.go tmessage.go tmessagetype.go tnonblocking_server.go tnonblocking_server_socket.go tnonblocking_socket.go tnonblocking_transport.go tnumeric.go tprocessor.go tprocessor_factory.go tprotocol.go tprotocol_exception.go tprotocol_factory.go tserver.go tserver_socket.go tserver_transport.go tset.go tsimple_server.go tsimple_json_protocol.go tsocket.go tstruct.go ttransport.go ttransport_exception.go ttransport_factory.go ttype.go
rm -f _obj/thrift.a
gopack grc _obj/thrift.a _go_.8
cp _obj/thrift.a "/home/masato/lib/google-go/pkg/linux_386/thrift.a"
make[1]: Leaving directory `/home/masato/lib/thrift/lib/go/thrift'

こんな感じで上手く行く。スバラシイ。

Thrift修正差分

ついでにどういう差分が適用されたかを見てみる。下はdiffをとった結果の一部だけど、net.Dialのところに注目するとちゃんと引数が減らされてる。

-  var err os.Error
-  if p.conn, err = net.Dial(p.addr.Network(), "", p.addr.String()); err != nil {
-    LOGGER.Print("Could not open socket", err.String())
-    return NewTTransportException(NOT_OPEN, err.String())
-  }
-  if p.conn != nil {
-    p.conn.SetTimeout(p.nsecTimeout)
-  }
-  return nil
+       var err os.Error
+       if p.conn, err = net.Dial(p.addr.Network(), p.addr.String()); err != nil {
+               LOGGER.Print("Could not open socket", err.String())
+               return NewTTransportException(NOT_OPEN, err.String())
+       }
+       if p.conn != nil {
+               p.conn.SetTimeout(p.nsecTimeout)
+       }
+       return nil

まとめ

ということで、Thriftという実例をつかってGofixの便利さを体感してみた。こういう便利ツールが公式に提供されて、かつほとんどが自動的に修正されるのであればAPIやら文法の変更もバンバンやってくれて構わないかも。

ちなみに、Thriftが生成したCassandraへのインタフェースライブラリをGofixしてもコンパイルが通らなかったことに関しては、また別の話。これThriftのバグなのか…? いまいち追う気になれない。

2011年4月9日土曜日

OCaml Pattern Matching

引きつづきOCaml。今度はパターンマッチング。パターンマッチング自体はその名の通り、与えられた引数の値に応じた値を返す関数を定義する仕組み。

組込み型でのパターンマッチング

(* normal pattern matching *)
let color_to_num = function
    "RED" -> 0
  | "BLUE" -> 1
  | "GREEN" -> 2
  | _ -> -1;;

let num = color_to_num "RED" in
Printf.printf "%d\n" num;;

let num = color_to_num "aardvark" in
Printf.printf "%d\n" num;;

出力

$ ./a.out
0
-1

独自型でのパターンマッチング

(* matching via Sum type*)
type color_t = Red | Blue | Green ;;
let color_t_to_num = function
    Red -> 0
  | Blue -> 1
  | Green -> 2;;

let num = color_t_to_num Blue in
Printf.printf "%d\n" num;;

(* Compile error
let num = color_t_to_num Black in
Printf.printf "%d\n" num;;

let num = color_t_to_num "aardvark" in
Printf.printf "%d\n" num;;
*)

出力

$ ./a.out
1

やっぱ型チェックが厳しいと安心してコードがかけるのがいい。正直Pythonとかはもうあまり書きたくないかも。

パターンマッチングの文法に関しては、let xxxx = function ... という書きかたと let xxxx = match x with ... という書きかたの使いわけが良くわからない。ただ、Oreillyの本で読むかぎりだとfunctionを使う方は単なるシンタックスシュガーっぽい。

2011年4月3日日曜日

Object Oriented OCaml

引きつづきOCaml。OCamlのObject Orientedな部分の仕様が思ってたより膨大で、Thriftが生成したコードが理解できないのでOreillyの本を読みつつサンプルを書いてみる。

oo.ml

class car (name, color)=
  object
      val mutable m_name = name
      val mutable m_color = color
      method get_name = m_name
      method get_color = m_color
      method to_string = m_color ^ " " ^ m_name
  end;;

class flying_car(name, color, flying) =
  object
      inherit car(name, color)
      val mutable m_flying = flying
      method land_car = m_flying <- false
      method fly_car = m_flying <- true
      method to_string =
        let state = if m_flying then "flying" else "not flying" in
        state ^ " " ^ m_color ^ " " ^ m_name
  end;;

let c = new flying_car("Keitora", "black", true) in
let () = c#land_car in
let msg = "I bought a new " ^ c#to_string in
print_endline msg

出力

$ ocamlc oo.ml
$ ./a.out
I bought a new not flying black Keitora

しかしOOな仕様が入ると途端に予約語が増える。ちなみに、最初は関数land_carをlandって名前にしてて謎のコンパイルエラーが発生してた。結局原因はlandという名前の組込み演算子が存在するから。syntax errorってだけじゃなかなか気付かないッスよコンパイラさん。

2011年3月27日日曜日

Konnichiwa OCaml

Goの言語仕様が変わって周辺ライブラリのコンパイルが通らなくなったので、とりあえずGoは放置してOCamlでThriftを試すことにした。その前段階としてOCamlの適当なサンプルでも書いてみる。

OCaml自体は結構前から知ってたけど、使う機会も無いしなんとなく書きかたが気持ちわるかったから敬遠してた。まあこの際だから試そう。

環境のセットアップ

Ubuntuならラクチン。

$ sudo apt-get install ocaml ocaml-mode

hello.ml

(* util functions *)
let concat (x, y) = 
  x ^ " " ^ y;;

(* main function *)
let msg = "konnichiwa" in
let name = "aardvark" in
let con = concat(msg, name) in
    print_endline con ;;

Makefile

コンパイルすると*.cmiというインタフェースの情報を記述したファイルと*.cmoというオブジェクトファイルができるけど、単純に実行バイナリを作るのに必要なのは*.cmoだけっぽい。実際、ソースをコンパイルしたあとにできる*.cmiファイルを消して、そのうえでリンクしても問題無かった。

BIN=hello
SRC=hello.ml
CAMLC=ocamlc
OBJ=$(SRC:.cmo=.ml)

$(BIN): $(OBJ)
 $(CAMLC) -o $(BIN) $(OBJ)

.ml.cmo:
 $(CAMLC) -c $<

clean:
 rm -f *.cm[iox] *~ .*~ #*#
 rm -f $(BIN)

出力

$ make
ocamlc -o hello hello.ml
$ file hello.cmi
hello.cmi: OCaml interface file (.cmi) (Version 011)
$ file hello.cmo
hello.cmo: OCaml object file (.cmo) (Version 007)
$ ./hello
konnichiwa aardvark

末尾のセミコロン二つがちょっと気持ち悪い。

2011年2月27日日曜日

Trying thrift4go with Cassandra

2010/3/26追記:Goのrelease.2011-03-07.1とThriftのr1085676だとコンパイル通らなくなってる。たぶんGoの文法が(また)変わったのが原因。

Go用にコードを生成するthrift4goがThrift本体に取り込まれたので、早速それをCassandraとの連携で試してみることにする。試したのはThriftのr1075010。

Thriftのビルド

何はともあれThriftをビルドしないことにははじまらないので、SVNからソースを落としてきてビルドする。

$ svn co http://svn.apache.org/repos/asf/thrift/trunk thrift
$ cd thrift
$ ./bootstrap.sh
$ ./configure --with-go --with-cpp --without-java --without-php --without-python --without-erlang
$ make -j2
$ sudo make install
$ cd lib/go
$ make && make install

注意点としては、なぜか単純にconfigure -> make -> make installしただけではGo用のライブラリがビルドされないので、手動でGoのライブラリをmake installしないといけないこと。これは単にconfigureスクリプトまわりのバグか、自分のやり方が悪いかのどっちかだけどとりあえず放置して先に進む。

ちなみに、Goのライブラリをインストールすると$GOROOT/pkg/linux_386/といった場所(このパスは環境に依存する)にthrift.aという名前でライブラリのバイナリが配置される。ここに配置されることによって、Goのプログラムから'import "thrift"'という感じでパッケージのインポートができるようになるけど、もしこのパスに置かずに直接インポートしようと思ったらthrift.aへのパスを明示的に指定しないといけない。

  • $GOROOT/pkg/linux_386/にthrift.aがある場合
  • package main
    import (
        "thrift"
    )
    ...
    
  • $GOROOT/pkg/linux_386/以外の場所(例えばプログラムと同じディレクトリ)にthrift.aがある場合
  • package main
    import (
        "./thrift"
    )
    ...
    

ThriftによるCassandraコードの生成

Thriftのビルドとインストールは終わったので、今度はCassandraのインタフェース定義からGoのコードを生成してもらって、コンパイルする。

$ thrift --gen go ~/lib/cassandra/interface/cassandra.thrift
$ cd gen-go/cassandra/
$ make
8g -o _go_.8 ttypes.go Cassandra.go
Cassandra.go:89: syntax error: unexpected range, expecting )
Cassandra.go:99: syntax error: unexpected name, expecting )
Cassandra.go:109: syntax error: unexpected name, expecting )
Cassandra.go:121: syntax error: unexpected name, expecting )
...

とここでコンパイルエラー。ソースを見てみたら、どうやら変数名に予約語のrangeが使用されてるせいでコンパイルエラーが起きている様子。ということでcassandra.thriftの定義を少々いじる。

$ diff cassandra.thrift cassandra.thrift.org
421c421
<                                   3:required KeyRange key_range,
---
>                                   3:required KeyRange range,

そこから改めてコード生成してコンパイルしたら上手くいった。

$ thrift --gen go ~/lib/cassandra/interface/cassandra.thrift
$ cd gen-go/cassandra/
$ make
8g -o _go_.8 ttypes.go Cassandra.go
rm -f _obj/thriftlib/cassandra.a
gopack grc _obj/thriftlib/cassandra.a _go_.8
cp _obj/thriftlib/cassandra.a "/home/masato/lib/google-go/pkg/linux_386/thriftlib/cassandra.a"
$
(続く)

2011年2月26日土曜日

Go + SWIG + Cassandra + Thrift (5)

つい最近thrift4goなんてのがThrift本体に取り込まれたらしい。なので、あえてC++で書いものをSWIGでラップする必要性は薄れたかもしれないけど、とりあえず最後まで書いていく。

さて、前回まででSWIG用のインタフェース定義を書き終わったので今度はそれを実際にビルドして、Goから使うところまでをやっていく。

SWIGでのビルド方法

基本的には公式に書かれている手順に従えばそのままビルドできる。ただ手順が複雑なので、手でやるのはあまり現実的でない。Makefileなりを書くのがいいと思う。ちなみに自分はSConsやらCMakeを使ったビルドもちょっと挑戦したけど、SWIGとのうまい連携が難しそうだったのでやっぱりMakefileで力技に落ちついた。

Makefile

簡単な流れを説明すると以下の通り。Makefileはあまり書き慣れてない(そもそもあまり好きでない)ので、そんなに参考にしないほうがいいかも。

  1. 自分で書いたラッパーをコンパイル。
  2. SWIGにラッパーコードを生成させてコンパイル。
  3. コンパイルしたものを共有ライブラリにまとめる。
  4. SWIGが生成したGoパッケージ用のコードをコンパイルしてパッケージ化。
PACKAGE_NAME=cassandra
SRC=Client.cpp CassandraAPI.cpp
OBJ=$(SRC:%.cpp=%.o)

THRIFT_SRC=Cassandra.cpp cassandra_constants.cpp cassandra_types.cpp
THRIFT_DIR=./gen-cpp
THRIFT_OBJ=$(THRIFT_SRC:%.cpp=%.o)

SWIG_OBJ=$(PACKAGE_NAME)_wrap.o
SWIG_SRC=$(PACKAGE_NAME)_wrap.cxx
CC=g++
LIBS=-lthrift

# i686
GO_ARCH=8

vpath %.cpp $(THRIFT_DIR)

all: lib golib

# C++ lib with SWIG
lib: $(OBJ) $(SWIG_OBJ) $(THRIFT_OBJ)
        $(CC) -shared $^ $(LIBS) -o $(PACKAGE_NAME).so
        cp $(PACKAGE_NAME).so ..

swig_interface: $(PACKAGE_NAME).i
        swig -Wall -c++ -go -I/usr/local/include/thrift $<

$(SWIG_OBJ): swig_interface $(SWIG_SRC)
        $(CC) -Wall -g -c -fpic -I/usr/local/include/thrift/ -I$(THRIFT_DIR) $(SWIG_SRC)

.cpp.o:
        $(CC) -Wall -g -c -fpic -I/usr/local/include/thrift/ -I$(THRIFT_DIR) $<

# Go lib
golib: $(PACKAGE_NAME).$(GO_ARCH) $(PACKAGE_NAME)_gc.$(GO_ARCH)
        gopack grc $(PACKAGE_NAME).a $^

$(PACKAGE_NAME)_gc.$(GO_ARCH):$(PACKAGE_NAME)_gc.c
        $(GO_ARCH)c -I $(GOROOT)/pkg/linux_386/ $<

$(PACKAGE_NAME).$(GO_ARCH): $(PACKAGE_NAME).go
        $(GO_ARCH)g $<

clean:
        rm -f *.o *.so *.8 *.a $(SWIG_SRC) $(PACKAGE_NAME)_gc.c $(PACKAGE_NAME).go

ビルド出力

SWIG実行時に大量に表示されるワーニングに関しては扱いが難しいところで、正しく直そうと思ったらThriftが生成したコードを手で修正しないといけない。自動生成されたコードを更に手で修正するのはあまり効率がいいとは思えないので、ひとまず放置するのが良い。

$ make
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp Client.cpp
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp CassandraAPI.cpp
swig -Wall -c++ -go -I/usr/local/include/thrift cassandra.i
gen-cpp/cassandra_types.h:26: Warning 314: 'type' is a Go keyword, renaming to 'Xtype'
gen-cpp/cassandra_types.h:38: Warning 314: 'type' is a Go keyword, renaming to 'Xtype'
gen-cpp/cassandra_types.h:46: Warning 314: 'type' is a Go keyword, renaming to 'Xtype'
/usr/local/include/thrift/Thrift.h:56: Warning 503: Can't wrap 'operator ++' unless renamed to a valid identifier.
/usr/local/include/thrift/Thrift.h:61: Warning 503: Can't wrap 'operator !=' unless renamed to a valid identifier.
/usr/local/include/thrift/Thrift.h:84: Warning 503: Can't wrap 'operator ()' unless renamed to a valid identifier.
gen-cpp/cassandra_types.h:59: Warning 451: Setting a const char * variable may leak memory.
(以下同様なwarningが数十行)
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp cassandra_wrap.cxx
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp ./gen-cpp/Cassandra.cpp
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp ./gen-cpp/cassandra_constants.cpp
g++ -Wall -g -c -fpic -I/usr/local/include/thrift/ -I./gen-cpp ./gen-cpp/cassandra_types.cpp
g++ -shared Client.o CassandraAPI.o cassandra_wrap.o Cassandra.o cassandra_constants.o cassandra_types.o -lthrift -o cassandra.so
cp cassandra.so ..
8g cassandra.go
8c -I /home/masato/lib/google-go/pkg/linux_386/ cassandra_gc.c
gopack grc cassandra.a cassandra.8 cassandra_gc.8

ラップしたライブラリをGoから呼ぶ

ここへ来てやってGoのコードが書ける。注意点としては、SWIGは特定のルールに基いて関数や型の名前をつけるので、良くわからない場合はSWIGが生成したコード、特にcassandra.goを見てGoからどうやって呼べばいいかを見る必要があるという点。

具体的に言うと、例えばラップしたクラスのなかにColumnPathというクラスがあって、それをGo側で生成したいと思った場合はcassandra.NewColumnPath()という関数を呼ばないといけないとか、ColumnPathクラスでpublicだったnameという変数にアクセスしようと思ったらGetName()でアクセスしないといけないだとか、そういう細かな話。この辺のルールはおそらく言語によって異なるうえ、SWIGのドキュメントにもあまり記述されてないっぽいので、生成されたコードを見た方がてっとり早い。

Goのサンプルコード

package main

import(
    "os"
    "log"
    "./lib/cassandra"
)

var logger *log.Logger = log.New(os.Stdout, "", 0)

func main() {
    defer func(){
        if err := recover(); err != nil {
            logger.Println("Error:",err)
        }
    }()

    client := cassandra.CreateClient("localhost", 9160)

    logger.Println("created client.")
    defer func(){
        cassandra.DestroyClient(client)
    }()

    logger.Printf("version: %s\n",client.GetVersion());

    keySpace := "sample_keyspace"
    client.SetKeySpace(keySpace)

    columnFamilyName := "SampleColumnFamily";
    columnName := "SampleName";
    path := cassandra.NewColumnPath()
    path.SetColumn_family(columnFamilyName)
    path.SetColumn(columnName)
    columnPathIsSet := cassandra.NewX_ColumnPath__isset()
    columnPathIsSet.SetColumn(true)
    path.SetX__isset(columnPathIsSet)
    key := "SampleColumn"
    column := cassandra.NewColumnOrSuperColumn()
    client.Get(column, key, path, cassandra.KOne)
    col := column.GetColumn()
    logger.Println("name:",col.GetName())
    logger.Println("value:",col.GetValue())
    logger.Println("timestamp:",col.GetTimestamp())
    logger.Println("TTL:",col.GetTtl())
}

出力

created client.
version: 19.4.0
name: SampleName
value: SampleValue
timestamp: 1297591990231000
TTL: 0

ちなみにわざと例外を発生させた場合は以下のような出力になる。見てわかるように、例外が発生した理由までちゃんと持ってこれている。これはSWIGのインタフェース定義に適切な例外処理を記述したおかげ。

$ ./sample
created client.
version: 19.4.0
Error: org::apache::cassandra::InvalidRequestException: Keyspace keyspaceThatDoesntExist does not exist

まとめ

以上、数回に渡ってGoとCassandraを連携させる方法を書いてきた。見てわかるように、現時点では非常に手順が多くて面倒なところが多い。特にSWIGはかなり仕様が膨大な上に資料も少ないので色々苦労すると思う。というかした。しかしSWIG自体はなにかと便利なので、覚えておくといつか役に立つ日がくるはず。

2011年2月20日日曜日

Go + SWIG + Cassandra + Thrift (4)

前回まででCassandraに入れたデータをC++で取り出せるようになった。今度はいよいよSWIGでC++をラップして、Goから呼ぶところをやっていく。

ラッパーの最上位APIを作成

ラップしたThriftそのものをGoから直接使用してもいいけど、今回はThriftの共通処理をC++側でラップしてしまって、単純な独自APIだけをGo側に公開していく方向にする。この方が(1)C++側で決まりきった処理を隠蔽できる、(2)色々泥臭いことも(やろうと思えば)C++側でできる、(3)自分で必要なものだけをGo側に公開することでGo側のコードが単純になる、といったメリットがある。

APIのざっくりとした設計

ライブラリのAPIはそれこそ色々考えられるけど、今回は以下のようなAPIを想定する。

package main
import (
    "cassandra"
)

fun main() {
    connectionA := cassandra.CreateClient("serverAtSomePlace.com", 6190)
    connectionB := cassandra.CreateClient("serverAtAnotherPlace.com", 6190)

    defer func() {
        // explicitly close connection before we exit
        cassandra.DestroyClient(connctionA)
        cassandra.DestroyClient(connectionB)
    }()

    awesomeData := connectionA.Get(/* some argument comes here*/)
    
    // process awesome data...
}

上の疑似コードで留意したのは主に以下の点。

  • ライブラリ内部で状態を持たない。状態を持つ必要があるもの(例えばネットワークの接続)は、APIによって作成されたオブジェクト内部に隠蔽して管理する。上の例で言うと、connectionAやconnectionBの中に接続状態が隠蔽されている。こうすると複数の接続を同時に管理できるし、初期化されてないライブラリ呼んじゃダメ、という制限が無くなる。

ラッパーのサンプルコード

以下が上の方針で書かれたサンプルコードである。

  • CassandraAPI.hpp
  • #ifndef CASSANDRA_CASSANDRAAPI_HPP
    #define CASSANDRA_CASSANDRAAPI_HPP
    
    #include <string>
    
    namespace cassandrawrap {
    
    class Client;
    
    Client* CreateClient(std::string, int);
    void DestroyClient(Client*);
    
    } // namespace
    
    #endif // CASSANDRA_CASSANDRAAPI_HPP
    
  • CassandraAPI.cpp
  • #include "CassandraAPI.hpp"
    #include "Client.hpp"
    using std::string;
    using cassandrawrap::Client;
    
    Client* cassandrawrap::CreateClient(std::string host, int port) {
        Client* client =  new Client();
        client->Open(host, port);
        return client;
    }
    
    void cassandrawrap::DestroyClient(Client* client) {
        if (client == NULL) {
            return;
        }
        client->Close();
        delete client;
    }
    
  • Client.hpp
  • #ifndef CASSANDRA_CLIENT_HPP
    #define CASSANDRA_CLIENT_HPP
    
    #include <string>
    #include <boost/shared_ptr.hpp>
    #include "ConsistencyLevel.hpp"
    #include "cassandra_types.h"
    
    namespace org { namespace apache { namespace cassandra {
    class CassandraClient;
    class ColumnOrSuperColumn;
    class ColumnPath;
    }}}
    
    namespace apache { namespace thrift { namespace protocol {
    class TProtocol;
    }}}
    
    namespace apache { namespace thrift { namespace transport {
    class TTransport;
    }}}
    
    
    namespace cassandrawrap {
    
    class Client {
    public:
        void Open(const std::string &, int);
        void Close();
        std::string GetVersion() const;
        void SetKeySpace(const std::string &);
        void Get(org::apache::cassandra::ColumnOrSuperColumn &,
                 const std::string &,
                 const org::apache::cassandra::ColumnPath &,
                 cassandrawrap::ConsistencyLevel) const;
    private:
        typedef boost::shared_ptr<apache::thrift::transport::TTransport> TTransportPtr;
        typedef boost::shared_ptr<apache::thrift::protocol::TProtocol> TProtocolPtr;
        typedef boost::shared_ptr<org::apache::cassandra::CassandraClient> CassandraClientPtr;
        TTransportPtr m_socket;
        TTransportPtr m_transport;
        TProtocolPtr m_protocol;
        CassandraClientPtr m_client;
    };
    
    } // namespace
    
    #endif // CASSANDRA_CONTEXT_HPP
    
  • Client.cpp
  • #include <protocol/TBinaryProtocol.h>
    #include <transport/TSocket.h>
    #include <transport/TTransportUtils.h>
    
    // auto-generated thrift interface code using cassandra.thrift in cassandra repository
    #include "Cassandra.h"
    #include "Client.hpp"
    
    using apache::thrift::protocol::TProtocol;
    using apache::thrift::protocol::TBinaryProtocol;
    using apache::thrift::transport::TSocket;
    using apache::thrift::transport::TTransport;
    //using apache::thrift::transport::TBufferedTransport;
    using apache::thrift::transport::TFramedTransport;
    using org::apache::cassandra::CassandraClient;
    using org::apache::cassandra::ColumnOrSuperColumn;
    using org::apache::cassandra::Column;
    using org::apache::cassandra::ColumnPath;
    using org::apache::cassandra::ConsistencyLevel;
    using org::apache::cassandra::KsDef;
    using org::apache::cassandra::InvalidRequestException;
    using org::apache::cassandra::NotFoundException;
    using apache::thrift::TException;
    using std::string;
    using cassandrawrap::Client;
    
    void Client::Open(const string &address, int port) {
        m_socket = TTransportPtr(new TSocket(address.c_str(), port));
        m_transport = TTransportPtr(new TFramedTransport(m_socket));
        m_protocol = TProtocolPtr(new TBinaryProtocol(m_transport));
        m_client = CassandraClientPtr(new CassandraClient(m_protocol));
        m_transport->open();
    }
    
    void Client::SetKeySpace(const string &keyspace) {
        m_client->set_keyspace(keyspace);
    }
    
    static org::apache::cassandra::ConsistencyLevel::type ConvertConsistencyLevel(cassandrawrap::ConsistencyLevel level) {
        return static_cast<org::apache::cassandra::ConsistencyLevel::type>(level);
    }
    
    void Client::Get(org::apache::cassandra::ColumnOrSuperColumn &output,
                     const std::string &key,
                     const org::apache::cassandra::ColumnPath &path,
                     cassandrawrap::ConsistencyLevel level) const {
    
        m_client->get(output,
                      key,
                      path,
                      ConvertConsistencyLevel(level));
    }
    
    string Client::GetVersion() const {
        string version;
        m_client->describe_version(version);
        return version;
    }
    
    void Client::Close(){
        m_transport->close();
    }
    
  • ConsistencyLevel.hpp
  • #ifndef CASSANDRA_CONSISTENCYLEVEL_HPP
    #define CASSANDRA_CONSISTENCYLEVEL_HPP
    
    namespace cassandrawrap {
    
    enum ConsistencyLevel {
        // Values are identical to the thrift definition
        kOne = 1,
        kQuorum = 2,
        kLocalQuorum = 3,
        kEachQuorum = 4,
        kAll = 5,
        kAny = 6
    };
    
    } // namespace
    
    #endif // CASSANDRA_CONSISTENCYLEVEL_HPP
    

SWIGでラップする

いよいよSWIGでラップする部分である。SWIGで何かをラップする場合はまずインタフェース定義というものを作らないといけない。それができたあとはそのインタフェース定義をSWIGに通して、生成されたソースファイルと自分が書いたソースとをコンパイルして完成、ということになる。

インタフェース定義の書き方は、簡単に書くと以下の手順を踏むことになる。

  1. %moduleディレクティブでモジュール名を指定する。
  2. %{から%}の間にコンパイルするのに必要なヘッダを追加する。
  3. それ以外の部分にはSWIGが用意している汎用のインタフェース定義や、自分がラップしたいクラスやら関数の定義が記述されたヘッダを追加する。

ただSWIGは出力が結構カスタマイズできるので、自分好みになるよう色々試行錯誤することができる。今回自分が試したのは、C++で発生した例外をどうやってGoまで上げるかという点。Goにはpanic/recoverといった例外ハンドルの仕組みがあるので、C++で発生した例外でもC++側でcatchせずにGoまで上げた方がGoと親和性が高くなる。

で、色々試した結果が下のcassandra.iにある"%exception"ディレクティブである。このディレクティブを使うと、特定の関数(あるいは関数全部)がどう例外をハンドルするかを指定することができる。これをうまく使うことによって、各exceptionクラスが持っているwhatやらwhyなど、例外が起きた理由をGoのレイヤーまで伝えられるようになる。%exception指定しないと、生成された例外キャッチ部分はどのexceptionクラスが飛んできたかしかGoまで上げてくれず、理由まで見れないのであんまりイケてない。

ちなみに、%exception指定せずに例外キャッチを自動生成させるには、ヘッダと関数の定義に"void SetKeySpace(const std::string &) throw(const apache::thrift::TException);"といった感じで明示的に投げるexceptionを指定しないといけない。

  • cassandra.i(インタフェース定義)
  • %module cassandra
    %{
    // given headers and generated headers
    #include <protocol/TBinaryProtocol.h>
    #include <transport/TSocket.h>
    #include <transport/TTransportUtils.h>
    #include "Cassandra.h"
    
    // my headers
    #include "Client.hpp"
    #include "CassandraAPI.hpp"
    #include "SwigUtility.hpp"
    %}
    
    // added for std::string
    %include stl.i
    
    // added to use primitive types in Go
    %include stdint.i
    
     // added for exception handling
    %include "exception.i"
    
    // thrift related headers
    %include <Thrift.h> // for exception types
    %include <TApplicationException.h> // for exception types
    %include "gen-cpp/cassandra_types.h"
    
    // The following are custom exception handlers for my API
    
    // Basic exception handler
    %exception {
        try {
            $action
        } catch(const apache::thrift::TException &exp) {
            // any thrift related exception
            std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
            // any other exception
            std::string msg = createExceptionString("std::exception: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    }
    
    %exception cassandrawrap::Client::SetKeySpace {
        try {
            $action
        } catch(const org::apache::cassandra::InvalidRequestException &exp) {
            std::string msg = createExceptionString("org::apache::cassandra::InvalidRequestException: ", exp.why);
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const apache::thrift::TException &exp) {
            // any thrift related exception
            std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
            // any other exception
            std::string msg = createExceptionString("std::exception: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    }
    
    %exception cassandrawrap::Client::Get {
        try {
            $action
        } catch(const org::apache::cassandra::NotFoundException &exp) {
            std::string msg = createExceptionString("org::apache::cassandra::NotFoundException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const org::apache::cassandra::InvalidRequestException &exp) {
            std::string msg = createExceptionString("org::apache::cassandra::InvalidRequestException: ", exp.why);
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const apache::thrift::TException &exp) {
            // any thrift related exception
            std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
            // any other exception
            std::string msg = createExceptionString("std::exception: ", exp.what());
            SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    }
    
    // my headers
    %include "ConsistencyLevel.hpp"
    %include "CassandraAPI.hpp"
    %include "Client.hpp"
    
  • SwigUtility.hpp
  • #ifndef CASSANDRA_SWIGUTILITY_HPP
    #define CASSANDRA_SWIGUTILITY_HPP
    
    #include <string>
    
    // utility function for exception handling
    static inline std::string createExceptionString(std::string type, const char* msg) {
        std::string what(msg);
        return type + what;
    }
    
    static inline std::string createExceptionString(std::string type, std::string msg) {
        return type + msg;
    }
    
    #endif // CASSANDRA_SWIGUTILITY_HPP
    
  • cassandra.iで%exception指定した場合の自動生成コード
  • try {
          (arg1)->SetKeySpace((std::string const &)*arg2);
        } catch(const org::apache::cassandra::InvalidRequestException &exp) {
          std::string msg = createExceptionString("org::apache::cassandra::InvalidRequestException: ", exp.why);
          SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch(const apache::thrift::TException &exp) {
          // any thrift related exception
          std::string msg = createExceptionString("apache::thrift::TException: ", exp.what());
          SWIG_exception(SWIG_RuntimeError, msg.c_str());
        } catch (const std::exception &exp) {
          // any other exception
          std::string msg = createExceptionString("std::exception: ", exp.what());
          SWIG_exception(SWIG_RuntimeError, msg.c_str());
        }
    
  • cassandra.iで%exception指定しなかった場合の自動生成コード
  • try {
        (arg1)->SetKeySpace((std::string const &)*arg2);
      }
      catch(apache::thrift::TException const &_e) {
        (void)_e;
        _swig_gopanic("C++ apache::thrift::TException const exception thrown");
    
      }
    

(続く)