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");
    
      }
    

(続く)

0 件のコメント:

コメントを投稿