RMIは、 イントロスペクション対応のPLにとって非常に簡単なタスクです。 ただし、残念ながら、C ++は適用されません。
この出版物では、C ++プリプロセッサを使用して非常に使いやすいRMIを実装する可能性を示したいと思います。
問題の声明
1.間違いを犯さないように、最も単純な構文を提供します。
2.手順の識別(リンク)は、間違えないように、ユーザーに対して非表示にする必要があります。
3.構文は、使用されるC ++型に制限を課すべきではありません。
4.バージョン化された手順の可能性があるはずですが、既存のクライアントとの互換性が損なわれないようにする必要があります。
4番目の点について:
たとえば、すでに2つのadd / divプロシージャがあり、それぞれに1つのバージョンがあります。 新しいバージョンを追加するにはaddが必要です。 バージョンをもう1つ追加するだけの場合、クライアントプログラムが変更前に収集したプロシージャIDは変動します。
ツール選択
なぜなら 最終結果はC ++コードと組み合わせて使用することになっています。3つのオプションがあります。
- 構文を発明し、独自のコードジェネレータを作成します。
- C ++プリプロセッサを使用します。
- 準備が整ったものを探し、自分で仕上げます(必要な場合)。
それぞれのオプションについてそれぞれ説明します。
- コード生成の追加ステージが必要な理由
- 私はプリプロセッサが大好きで、よく使用します。
- 時間と労力の無駄。 そして、それが理にかなっているかは明らかではありません。
要件の最初、2番目、3番目の段落については、プリプロセッサバージョンが適しています。
したがって、選択が行われます-プリプロセッサを使用します。 そしてもちろん、 boost.preprocessorです。
プリプロセッサについて少し
C ++プリプロセッサデータ型:
ご覧のように、型は十分すぎるほどです。
少し考えて、それぞれの可能性と制限について読み、また、構文の望ましい単純さと間違いを犯さないことを考慮に入れて、選択はシーケンスとタプルを優先して行われました。
いくつかの実例。
(a)(b)(c)
-シーケンス。 ここでは、3つの要素で構成されるシーケンスについて説明しました。
(a)
もシーケンスですが、1つの要素で構成されます。 (注意!)
(a)(b, c)(d, e, f)
-再びシーケンスですが、3つのタプルで構成されます。 (最初の要素に注意を払います-ただし、トリックですが、これは本当にタプルです)
(a)(b, c)(d, (e, f))
-再びシーケンスであり、3つのタプルで構成されます。 しかし! 最後のタプルは2つの要素で構成されます:1)任意の要素、2)タプル。
そして最後に、そのような例:
(a)(b, c)(d, (e, (f)(g)))
-ここで自分でそれを理解することができます;)
どうやら、すべてが本当に非現実的にシンプルです。
プロトタイプ構文
少し考えた後、次の構文が現れます。
(proc_name0, // (signature_arg0, signature_arg1, signature_argN) // (signature_arg0) // ) (proc_name1, // (signature_arg0, signature_arg1) // ) (proc_name2, // () // ( ) )
まあ...しかし、非常に先制的です。
実装の詳細
なぜなら 要件の1つは、手順のバージョン管理であり、既存のクライアントとの互換性が損なわれないようにするためでも、手順を識別するために2つのIDが必要です。 最初はプロシージャID、2番目はバージョンIDです。
例で説明します。
これがサービスのAPIの説明だとしましょう。 このAPIを使用するクライアントプログラムが既にあるとします。
(proc_name0, // procID=0 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name1, // procID=1 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name2, // procID=2 () // sigID=0 )
ここで、
proc_name0()
に対して、異なる署名を持つ別のバージョンを追加する必要があります。
(proc_name0, // procID=0 (signature_arg0, signature_arg1) // sigID=0 (signature_arg0, signature_arg1, signature_arg2) // sigID=1 ) (proc_name1, // procID=1 (signature_arg0, signature_arg1) // sigID=0 ) (proc_name2, // procID=2 () // sigID=0 )
したがって、プロシージャの新しいIDバージョンがありますが、前者は変更されていません。
それは:(0:0)、それは:(0:0)(0:1)になりました
つまり これはまさに私たちが達成しようとしたものです。 以前のクライアントは両方使用(0:0)し、これらのプロシージャの新しいバージョンが登場することを心配せずにこれらの識別子を使用し続けます。
また、すべての新しい手順を最後に追加する必要があることに同意します。
次に、サービスの両側にIDが自動的に付加されるようにする必要があります。 簡単! -説明された同じシーケンスを2回使用して、クライアント側とサーバー側を生成します!
長い目で見れば、これらすべてをどのように見たいのかを想像する時が来ました。
MACRO( client_invoker, // name of the client invoker implementation class ((registration, // procedure name ((std::string, std::string)) // message : registration key )) ((activation, ((std::string)) // message )) ((login, ((std::string)) // message )) ((logout, ((std::string)) // message )) ((users_online, ((std::vector<std::string>)) // without args )) , server_invoker, // name of the server invoker implementation class ((registration, ((std::string)) // username )) ((activation, ((std::string, std::string, std::string)) // registration key : username : password )) ((login, ((std::string, std::string)) // username : password )) ((logout, (()) // without args )) ((users_online, (()) // without args ((std::string)) // substring )) )
だれがリーダーであり、誰がスレーブであるかについて混乱がないように、一方の当事者に記載されている手順は反対側の実装であることに同意します。 つまり、たとえば、
client_invoker::registration(std::string, std::string)
は、このプロシージャの実装はサーバー側にあり、このプロシージャへのインターフェースは側にあることを示しています顧客、およびその逆。
(プリプロセッサは、MACRO()の引数を生成するときに、記述されたAPIを拡張するため、二重括弧を使用します。克服できますが、必要かどうかはわかりませんか?)
まとめ
上記のマクロ呼び出しから、スポイラーの下にあるコードが生成されます。
コード
namespace yarmi { template<typename Impl, typename IO = Impl> struct client_invoker { client_invoker(Impl &impl, IO &io) :impl(impl) ,io(io) {} void yarmi_error(const std::uint8_t &arg0, const std::uint8_t &arg1, const std::string &arg2) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(0) & static_cast<std::uint8_t>(0) & arg0 & arg1 & arg2; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void registration(const std::string &arg0) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(1) & static_cast<std::uint8_t>(0) & arg0; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void activation(const std::string &arg0, const std::string &arg1, const std::string &arg2) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(2) & static_cast<std::uint8_t>(0) & arg0 & arg1 & arg2; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void login(const std::string &arg0, const std::string &arg1) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(3) & static_cast<std::uint8_t>(0) & arg0 & arg1; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void logout() { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(4) & static_cast<std::uint8_t>(0); yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void users_online() { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(5) & static_cast<std::uint8_t>(0); yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void users_online(const std::string &arg0) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(5) & static_cast<std::uint8_t>(1) & arg0; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void invoke(const char *ptr, std::size_t size) { std::uint8_t call_id, call_version; static const char* names[] = { "yarmi_error" ,"registration" ,"activation" ,"login" ,"logout" ,"users_online" }; static const std::uint8_t versions[] = { 0, 0, 0, 0, 0, 0 }; try { yas::binary_mem_iarchive ia(ptr, size, yas::no_header); ia & call_id & call_version; if ( call_id < 0 || call_id > 5 ) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"%s::%s(): bad call_id %d" ,"client_invoker" ,__FUNCTION__ ,static_cast<int>(call_id) ); throw std::runtime_error(errstr); } if ( call_version > versions[call_id] ) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"%s::%s(): bad call_version %d for call_id %d(%s::%s())" ,"client_invoker" ,__FUNCTION__ ,static_cast<int>(call_version) ,static_cast<int>(call_id) ,"client_invoker" ,names[call_id] ); throw std::runtime_error(errstr); } switch ( call_id ) { case 0: { std::uint8_t arg0; std::uint8_t arg1; std::string arg2; ia & arg0 & arg1 & arg2; impl.on_yarmi_error( arg0 , arg1 , arg2); }; break; case 1: { std::string arg0; std::string arg1; ia & arg0 & arg1; impl.on_registration(arg0, arg1); }; break; case 2: { std::string arg0; ia & arg0; impl.on_activation(arg0); }; break; case 3: { std::string arg0; ia & arg0; impl.on_login(arg0); }; break; case 4: { std::string arg0; ia & arg0; impl.on_logout(arg0); }; break; case 5: { std::vector<std::string> arg0; ia & arg0; impl.on_users_online(arg0); }; break; } } catch (const std::exception &ex) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"std::exception is thrown when %s::%s() is called: '%s'" ,"client_invoker" ,names[call_id] ,ex.what() ); yarmi_error(call_id, call_version, errstr); } catch (...) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"unknown exception is thrown when %s::%s() is called" ,"client_invoker" ,names[call_id] ); yarmi_error(call_id, call_version, errstr); } } private: Impl &impl; IO &io; }; // struct client_invoker template<typename Impl, typename IO = Impl> struct server_invoker { server_invoker(Impl &impl, IO &io) :impl(impl) ,io(io) {} void yarmi_error(const std::uint8_t &arg0, const std::uint8_t &arg1, const std::string &arg2) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(0) & static_cast<std::uint8_t>(0) & arg0 & arg1 & arg2; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void registration(const std::string &arg0, const std::string &arg1) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(1) & static_cast<std::uint8_t>(0) & arg0 & arg1; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void activation(const std::string &arg0) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(2) & static_cast<std::uint8_t>(0) & arg0; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void login(const std::string &arg0) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(3) & static_cast<std::uint8_t>(0) & arg0; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void logout(const std::string &arg0) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(4) & static_cast<std::uint8_t>(0) & arg0; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void users_online(const std::vector<std::string> &arg0) { yas::binary_mem_oarchive oa(yas::no_header); oa & static_cast<std::uint8_t>(5) & static_cast<std::uint8_t>(0) & arg0; yas::binary_mem_oarchive pa; pa & oa.get_intrusive_buffer(); io.send(pa.get_shared_buffer()); } void invoke(const char *ptr, std::size_t size) { std::uint8_t call_id, call_version; static const char* names[] = { "yarmi_error" ,"registration" ,"activation" ,"login" ,"logout" ,"users_online" }; static const std::uint8_t versions[] = { 0, 0, 0, 0, 0, 1 }; try { yas::binary_mem_iarchive ia(ptr, size, yas::no_header); ia & call_id & call_version; if ( call_id < 0 || call_id > 5 ) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"%s::%s(): bad call_id %d" ,"server_invoker" ,__FUNCTION__ ,static_cast<int>(call_id) ); throw std::runtime_error(errstr); } if ( call_version > versions[call_id] ) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"%s::%s(): bad call_version %d for call_id %d(%s::%s())" ,"server_invoker" ,__FUNCTION__ ,static_cast<int>(call_version) ,static_cast<int>(call_id) ,"server_invoker" ,names[call_id] ); throw std::runtime_error(errstr); } switch ( call_id ) { case 0: { std::uint8_t arg0; std::uint8_t arg1; std::string arg2; ia & arg0 & arg1 & arg2; impl.on_yarmi_error(arg0, arg1, arg2); }; break; case 1: { std::string arg0; ia & arg0; impl.on_registration(arg0); }; break; case 2: { std::string arg0; std::string arg1; std::string arg2; ia & arg0 & arg1 & arg2; impl.on_activation(arg0, arg1, arg2); }; break; case 3: { std::string arg0; std::string arg1; ia & arg0 & arg1; impl.on_login(arg0, arg1); }; break; case 4: { impl.on_logout(); }; break; case 5: { switch ( call_version ) { case 0: { impl.on_users_online(); }; break; case 1: { std::string arg0; ia & arg0; impl.on_users_online(arg0); }; break; } }; break; } } catch (const std::exception &ex) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"std::exception is thrown when %s::%s() is called: '%s'" ,"server_invoker" ,names[call_id] ,ex.what() ); yarmi_error(call_id, call_version, errstr); } catch (...) { char errstr[1024] = {0}; std::snprintf( errstr ,sizeof(errstr) ,"unknown exception is thrown when %s::%s() is called" ,"server_invoker" ,names[call_id] ); yarmi_error(call_id, call_version, errstr); } } private: Impl &impl; IO &io; }; // struct server_invoker } // ns yarmi
(私のもう1つのプロジェクトであるYASは、シリアル化として使用されます)
ボーナスとして、
yarmi_error()
システムプロシージャが追加されました。これは、呼び出しを試みているときにエラーが発生したことを反対側に通知するために使用されます。
client_invoker::invoke()
、逆シリアル化と呼び出しは
try{}catch()
ブロックでラップされ、
yarmi_error()
catch()
ブロックで呼び出されます。 したがって、プロシージャの逆シリアル化または呼び出し中に例外が発生すると、
catch()
ブロックによって例外が正常にキャッチされ、例外に関する情報が呼び出し元に送信されます。 同じことが逆方向にも起こります。 つまり サーバーが、例外が発生した呼び出し中にプロシージャをクライアントに呼び出した場合-クライアントは、エラー情報をサーバーに送信し、さらに例外が発生した呼び出しのIDとバージョンも報告します。 しかし、あなたは
yarmi_error()
自分で使うことができます。これは禁止されていません。 例:
yarmi_error(call_id, version_id, "message");
お気づきかもしれませんが、接頭辞
on_
、実装側で説明されているプロシージャの名前に追加されます。
client_invoker
および
server_invoker
は2つのパラメーターを受け入れます。 最初のボトムは呼び出されたプロシージャが実装されるクラスで、2番目は
send(yas::shared_buffer buf)
メソッド
send(yas::shared_buffer buf)
実装されるクラスです。
両方の役割を実行する同じクラスがある場合、これを行うことができます:
struct client_session: yarmi::client_base<client_session>, yarmi::client_invoker<client_session> { client_session(boost::asio::io_service &ios) :yarmi::client_base<client_session>(ios, *this) ,yarmi::client_invoker<client_session>(*this, *this) // <<<<<<<<<<<<<<<<<<<<<<<<< {} };
最終バージョンは次のようになります。
struct client_session: yarmi::client_base<client_session>, yarmi::client_invoker<client_session> { client_session(boost::asio::io_service &ios) :yarmi::client_base<client_session>(ios, *this) ,yarmi::client_invoker<client_session>(*this, *this) {} void on_registration(const std::string &msg, const std::string ®key) {} void on_activation(const std::string &msg) {} void on_login(const std::string &msg) {} void on_logout(const std::string &msg) {} void on_users_online(const std::vector<std::string> &users) {} };
反対側へのインターフェースは、
yarmi::client_invoker
祖先から継承されます。 つまり、たとえば、
client_session
のコンストラクターにいる場合、次のように
registration()
プロシージャを呼び出すことができます。
{ registration("niXman"); }
実装
client_session::on_registration(std::string msg, std::string regkey)
答えを取得します
それだけです!
欠点のうち、次のことに注意する必要があります。
プリプロセッサはプロシージャが使用されるコンテキストを理解しないため、プロシージャを記述する型名にコンマを使用することはできません。 修正されます。
最終的に、これらすべてがYARMI (Yet Another RMI)と呼ばれるプロジェクトをもたらしました。
説明されているコードジェネレーターは、1つのファイルyarmi.hppにエンコードされています。 合計で、コードジェネレーターの実装には1営業日かかりました。
この全体の使用例は、 こことここで見ることができます 。 残念ながら、最初のテストプロジェクトはまだ完了していません。
説明に加えて、プロジェクトページには、非同期マルチユーザーシングルスレッドサーバーのコードとクライアントコードがあります。
結論の代わりに
計画:
1.複数のインターフェースの生成
2.仕様を説明します(ただし、どこでも簡単ではありません)
3.独自のロケーターを使用する機能
建設的な批判と提案に感謝します。
PS
このコードは、ゲーム開発のいくつかの商用プロジェクトで使用されています。