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自体はなにかと便利なので、覚えておくといつか役に立つ日がくるはず。

0 件のコメント:

コメントを投稿