C++
RPC
C++Day 9

C++ で RPC(Remote Procedure Call)

More than 1 year has passed since last update.


RPC とは

RPC(Remote Procedure Call) というのは、

別のプロセスや、別のコンピュータ上にある関数を呼び出す技術です。

C++ で RPC を実現するアイデアを思いついたので実装しました。


先行研究


インターフェイスとプロキシクラス

よくある実現手段として、

インターフェイスとプロキシクラスを使う方法があります。

サーバ側ではインターフェイスを公開し、

クライアントはそのインターフェイスの関数を呼び出します。

C++ でインターフェイスというと

こんな感じの抽象クラスを思い浮かべるでしょう。

class service_interface

{
public:
virtual void foo() = 0;
virtual void bar(int) = 0;
};

サーバ側ではこのインターフェイスを実装して、

クライアントはインターフェイス越しにインスタンスにアクセスすることになります。

しかし、抽象クラスが存在するだけでは外部からアクセスできません。

HTTPサーバや、プロセス間通信を行うライブラリに

このクラスや関数の存在を伝える必要があります。

また、クライアント側にもこのインターフェイスを実装した、

関数が呼び出されるたびに通信して、ローカルのインスタンスのように見せかける

プロキシクラスが必要になります。


既存の実現手段

私が知っている実現手段として以下のものがあります。


  1. インターフェイスを定義したファイルからソースコードを生成する

  2. リフレクションでインターフェイスを認識し、動的コード生成でプロキシクラスを生成する

  3. 厳密なインターフェイスを使用しない

COM(Component Object Model) や gRPC は 1. の方法を採用しています。

様々な言語に対応したいときには便利なのですが、

専用のプログラムで生成したソースコードを編集、コンパイルする必要があり、

手組みのソースコードだけでは完結しません。

Java や C# のように、リフレクションと動的コード生成を備えた言語では

2. の方法を採用することができますが、C++ では難しいでしょう。

MessagePack-RPC では 3. の方法を採用しているようです。

気軽に利用できて便利なのですが、

間違った型を渡してもコンパイルエラーとならず、

実行時にエラーが発生する可能性があります。


提案手法

class service_interface

{
public:
virtual void foo() = 0;
virtual void bar(int) = 0;
};

上は最初のインターフェイスですが、

下のように書き換えても良いのではないでしょうか。

struct service_interface

{
std::function<void()> foo;
std::function<void(int)> bar;
};

このようにインターフェイスを抽象クラスではなく

関数オブジェクトの集合として扱っても、

同様の書き方で関数を呼び出すことができます。

service_interface s = ...;

s.foo();
s.bar(42);

では、どうすればこの構造体のインスタンスを

取得することができるでしょうか。


シリアライゼーション

RPC にも関連する技術に、シリアライゼーション(直列化)があります。

インスタンスをバイト列に変換するシリアライズ処理と、

バイト列からインスタンスを復元するデシリアライズ処理から成ります。

サーバ側でインスタンスをシリアライズし、

クライアント側でデシリアライズすることができれば、

インスタンスの受け渡しが可能ですが、

先程の service_interface ではどうでしょうか。

シリアライゼーションライブラリとして cereal を使用します。

struct service_interface

{
std::function<void()> foo;
std::function<void(int)> bar;

template <class Archive>
void serialize(Archive& ar)
{
ar(foo, bar);
}
};

このコードには問題があります。

通常、std::function をシリアライズすることはできないため

このクラスのインスタンスをシリアライズしようとすると

コンパイルエラーが発生してしまいます。

逆に言えば、std::function さえシリアライズできるようになれば

この方法で RPC が可能となります。

std::function をシリアライズするにはどうしたらよいでしょうか。


アーカイブ

cereal や Boost.Serialization ではアーカイブクラスを自作することで

独自フォーマットでのシリアライズが可能です。

専用アーカイブを使って、以下の方法で

std::function をシリアライズすることができます。

まず、サーバ側、クライアント側の両方に「関数マップ」を用意します。

専用アーカイブが関数をシリアライズするときは、

関数マップに関数を登録して、番号を取得します。

取得した番号をバイト列として書き込みます。

専用アーカイブが関数をデシリアライズするときは、

バイト列から番号を読み込み、

「コネクション経由で、関数番号と、与えられた引数をシリアライズして送る関数」

にデシリアライズします。

コネクション経由で関数番号とシリアライズされた引数を受け取った側は、

関数マップから関数を検索し、引数をデシリアライズして渡します。


利点

この方法には以下の利点があります。


  • 手組みのソースコードだけで完結する

  • しっかり型チェックできる

  • シリアライザのバージョニング機能によって、互換性を保った仕様変更が可能


実装

実装してみました。

関数の戻り値には対応していません。

ox - An RPC library for C++