最近の C++(-std=c++14)MessagePack-RPC を再実装してみる。

基本設計

MessagePack-RPCの仕様をおさらいすると以下の通り。

request

[type, msgid, method, params]
(0) (int) (str) (array)

response

[type, msgid, error, result]
(1) (int) (any) (any)

msgpackのバイト列を受け取って、msgpackのバイト列を返す関数として一般化する。

typedef std::vector<std::uint8_t> bytes;
// msgpackのバイト列を引数にとり、msgpackのバイト列を返す
typedef std::function<bytes(const &bytes)> procedurecall;

任意の関数呼び出しからprocedurecallを作り出せるようにして、MessagePack-RPCシステムの部品として使えるようにする。 簡単な例 例として

static int add(int a, int b){ return a+b; }

をprocedurecallに変換してみる。

procedurecall make_procedurecall(int(*f)(int, int))
{
// request -> response ではなくparams -> result
return [f](const bytes& src)->bytes
{
// unpack args
auto parser = msgpackpp::parser(src);
std::tuple<int, int> args;
parser >> args;
// call
auto r = f(std::get<0>(args), std::get<1>(args));
// pack result
msgpackpp::packer packer;
packer << r;
return packer.get_payload();
};
}

int add(int, int)procedurecall に変換するというのは、引数のアンパック、関数呼び出し、結果のパックという一連の定型コードの呼び出しになる。

procedurecall の使い方は以下の通り。

// register
auto proc = msgpackpp::rpc::make_procedurecall(&add);
// call
auto packer = msgpackpp::packer();
packer << std::make_tuple(1, 2);
auto result = proc(packer.get_payload());
// result
REQUIRE(3 == msgpackpp::parser(result).get_number<int>());

とりあえず動いたが、関数を増やすたびにこれだけのコードを記述するのはやってられませぬ。 以下のような理想形を目指して作りこんでゆく。

REQUIRE(3 == msgpack_procedurecall([](int a, int b){ return a+b; }, 1, 2));

lambda が動けば他も動くようにできるので、lambda を第一に実装する。

実装

ステップ毎に説明しようと思っていたが分かりにくいので、コードにコメントを追加することにした。

template<typename F, typename R, typename C, typename ...AS, std::size_t... IS>
procedurecall _make_procedurecall(const F &f
, R(C::*)(AS...)const // template引数R, C, ASを受け付けるためのダミー
, std::index_sequence<IS...> // template引数ISを受け付けるためのダミー
)
{
// request -> response ではなくparams -> result
return [f](const bytes& src)->bytes
{
// unpack args
auto parser = msgpackpp::parser(src);
std::tuple<AS...> args;
parser >> args;
// call
auto r = f(std::get<IS>(args)...); // 可変長テンプレート引数を展開できる。ISと...が離れていることに注意
// pack result
msgpackpp::packer packer;
packer << r;
return packer.get_payload();
};
}
template<typename F, typename R, typename C, typename ...AS>
procedurecall _make_procedurecall(F f
, R(C::*)(AS...)const // template引数R, C, ASを受け付けるためのダミー
)
{
return _make_procedurecall(f
, &decltype(f)::operator() // lambdaの返り値と引数の型を次のテンプレートに渡す
, std::index_sequence_for<AS...>{} // std::get呼び出しのためにindex_sequenceを作る。
);
}
//
// あらゆる型のlambdaを受け付けるようにした
//
template<typename F>
procedurecall make_procedurecall(F f)
{
return _make_procedurecall(f
, &decltype(f)::operator() // lambdaの返り値と引数の型を次のテンプレートに渡す
);
}
msgpack_call
template<typename F, typename R, typename C, typename ...AS>
decltype(auto) _msgpack_call(F f
, R(C::*)(AS...)const // template引数R, C, ASを受けるためのダミー
, AS... args)
{
auto proc = msgpackpp::rpc::make_procedurecall(f);
// call
msgpackpp::packer packer;
packer << std::make_tuple(args...); // 可変長テンプレート引数を展開できる
auto result = proc(packer.get_payload());
// unpack result
R value;
msgpackpp::parser(result) >> value;
return value;
}
template<typename F, typename ...AS>
decltype(auto) msgpack_call(F f, AS... args) // 返り値の型はreturnから型推論
{
return _msgpack_call(f
, &decltype(f)::operator() // lambdaの返り値と引数の型をテンプレート引数に渡す
, args...
);
}

使う。

REQUIRE(3==msgpack_call([](int a, int b) { return a + b; }, 1, 2));
REQUIRE(-1==msgpack_call([](int a, int b) { return a - b; }, 1, 2));

valiadic template おそるべし。 従来であれば、1引数、2引数・・・と引数の個数ごとに手作業でバージョンを増やさねばならなかったものが、わりとさくっと書けるな。