<p>This was initially posted on my blog at:&nbsp;&nbsp;<a href="http://www.crazygaze.com/blog/2016/06/06/modern-c-lightweight-binary-rpc-framework-without-code-generation/">http://www.crazygaze.com/blog/2016/06/06/modern-c-lightweight-binary-rpc-framework-without-code-generation/</a></p> <h1>Table of Contents</h1> <ol> <li><a href="#id-introduction">Introduction</a> <ol> <li><a href="#id-why-i-needed-this">Why I needed this</a></li> </ol> </li> <li><a href="#id-rpc-parameters">RPC parameters</a> <ol> <li><a href="#id-parameter-traits">Parameter Traits</a></li> <li><a href="#id-serialization">Serialization</a></li> <li><a href="#id-deserialization">Deserialization</a></li> <li><a href="#id-from-tuple-to-function-parameters">From tuple to function parameters</a></li> </ol> </li> <li><a href="#id-the-rpc-api">The RPC API</a> <ol> <li><a href="#id-header">Header</a></li> <li><a href="#id-table">Table</a></li> <li><a href="#id-transport">Transport</a></li> <li><a href="#id-result">Result</a></li> <li><a href="#id-out-processor">OutProcessor</a></li> <li><a href="#id-in-processor">InProcessor</a></li> <li><a href="#id-connection">Connection</a></li> </ol> </li> <li><a href="#id-improvements">Improvements</a></li> </ol> <p><a name="id-introduction"></a></p> <h2>Introduction</h2> <p><img alt="" class="alignnone size-full wp-image-652" src="http://www.crazygaze.com/blog/wp-content/uploads/2016/06/img_57561179204f5.png" /></p> <p>This article explores a C++ RPC framework I&#39;ve been working on which requires no code generation step for glue code. Before I start rambling on implementation details, and so you know what to expect, here is a feature list:</p> <ul> <li>Source available at <a href="https://bitbucket.org/ruifig/czrpc">https://bitbucket.org/ruifig/czrpc</a> <ul> <li>The source code shown in this article is by no means complete. It&#39;s meant to show the foundations upon which the framework was built. Also, to shorten things a bit, it&#39;s a mix of code from the repository at the time of writing and custom sample code, so it might have errors.</li> <li>Some of the source code which is not directly related to the problem at hand is left intentionally simple with disregard for performance. Any improvements will later be added to source code repository.</li> </ul> </li> <li>Modern C++ (C++11/14) <ul> <li>Requires at least <strong>Visual Studio 2015</strong>. Clang/GCC is fine too, but might not work as-is, since VS is less strict.</li> </ul> </li> <li>Type-safe <ul> <li>The framework detects at <strong>compile time</strong> invalid RPC calls, such as unknown RPC names, wrong number of parameters, or wrong parameter types.</li> </ul> </li> <li>Relatively small API and not too verbose (considering it requires no code generation)</li> <li>Multiple ways to handle RPC replies <ul> <li>Asynchronous handler</li> <li>Futures</li> <li>A client can detect if an RPC caused an exception server side</li> </ul> </li> <li>Allows the use of potentially any type in RPC parameters <ul> <li>Provided the user implements the required functions to deal with that type.</li> </ul> </li> <li>Bidirectional RPCs (A server can call RPCs on a client) <ul> <li>Typically, client code cannot be trusted, but since the framework is to be used between trusted parties, this is not a problem.</li> </ul> </li> <li>Non intrusive <ul> <li>An object being used for RPC calls doesn&#39;t need to know anything about RPCs or network.</li> <li>This makes it possible to wrap third party classes for RPC calls.</li> </ul> </li> <li>Minimal bandwidth overhead per RPC call</li> <li>No external dependencies <ul> <li>Although the supplied transport (in the source code repository) uses Asio/Boost Asio, the framework itself does not depend on it. You can plug in your own transport.</li> </ul> </li> <li>No security features provided <ul> <li>Because the framework is intended to be used between trusted parties (e.g: between servers).</li> <li>The application can specify its own transport, therefore having a chance to encrypt anything if required.</li> </ul> </li> </ul> <p>Even though the source code shown is not complete, the article is still very heavy on code. Code is presented in small portions and every section builds on the previous, but is still an overwhelming amount of code. So that you have an idea of how it will look like in the end, here is a fully functional sample using the source code repository at the time of writing:</p> <pre> <code class="language-cpp">////////////////////////////////////////////////////////////////////////// // Useless RPC-agnostic class that performs calculations. ////////////////////////////////////////////////////////////////////////// class Calculator { public: double add(double a, double b) { return a + b; } }; ////////////////////////////////////////////////////////////////////////// // Define the RPC table for the Calculator class // This needs to be seen by both the server and client code ////////////////////////////////////////////////////////////////////////// #define RPCTABLE_CLASS Calculator #define RPCTABLE_CONTENTS \ REGISTERRPC(add) #include "crazygaze/rpc/RPCGenerate.h" ////////////////////////////////////////////////////////////////////////// // A Server that only accepts 1 client, then shuts down // when the client disconnects ////////////////////////////////////////////////////////////////////////// void RunServer() { asio::io_service io; // Start thread to run Asio's the io_service // we will be using for the server std::thread th = std::thread([&amp;io] { asio::io_service::work w(io); io.run(); }); // Instance we will be using to serve RPC calls. // Note that it's an object that knows nothing about RPCs Calculator calc; // start listening for a client connection. // We specify what Calculator instance clients will use, auto acceptor = AsioTransportAcceptor&lt;Calculator, void&gt;::create(io, calc); // Start listening on port 9000. // For simplicity, we are only expecting 1 client using ConType = Connection&lt;Calculator, void&gt;; std::shared_ptr&lt;ConType&gt; con; acceptor-&gt;start(9000, [&amp;io, &amp;con](std::shared_ptr&lt;ConType&gt; con_) { con = con_; // Since this is just a sample, close the server once the first client // disconnects reinterpret_cast&lt;BaseAsioTransport*&gt;(con-&gt;transport.get()) -&gt;setOnClosed([&amp;io] { io.stop(); }); }); th.join(); } ////////////////////////////////////////////////////////////////////////// // A client that connects to the server, calls 1 RPC // then disconnects, causing everything to shut down ////////////////////////////////////////////////////////////////////////// void RunClient() { // Start a thread to run our Asio io_service asio::io_service io; std::thread th = std::thread([&amp;io] { asio::io_service::work w(io); io.run(); }); // Connect to the server (localhost, port 9000) auto con = AsioTransport&lt;void, Calculator&gt;::create(io, "127.0.0.1", 9000).get(); // Call one RPC (the add method), specifying an asynchronous handler for // when the result arrives CZRPC_CALL(*con, add, 1, 2) .async([&amp;io](Result&lt;double&gt; res) { printf("Result=%f\n", res.get()); // Prints 3.0 // Since this is a just a sample, stop the io_service after we get // the result, // so everything shuts down io.stop(); }); th.join(); } // For testing simplicity, run both the server and client on the same machine, void RunServerAndClient() { auto a = std::thread([] { RunServer(); }); auto b = std::thread([] { RunClient(); }); a.join(); b.join(); }</code></pre> <p>This code is mostly setup code, since the provided transport uses Asio. The RPC calls itself can be as simple as:</p> <pre> <code class="language-cpp">// RPC call using asynchronous handler to handle the result CZRPC_CALL(*con, add, 1, 2).async([](Result&lt;double&gt; res) { printf("Result=%f\n", res.get()); // Prints 3.0 }); // RPC call using std::future to handle the result Result&lt;double&gt; res = CZRPC_CALL(*con, add, 1, 2).ft().get(); printf("Result=%f\n", res.get()); // Prints 3.0</code></pre> <p><a name="id-why-i-needed-this"></a></p> <h3>Why I needed this</h3> <p>The game I&#39;ve been working on for a couple of years now (<a href="https://bitbucket.org/ruifig/g4devkit">code named G4</a>), gives players fully simulated little in-game computers they can code for whatever they want. That requires me to have a couple of server types running:</p> <ul> <li>Gameplay Server(s)</li> <li>VM Server(s) (Simulates the in-game computers) <ul> <li>So that in-game computers can be simulated even if the player is not currently online</li> </ul> </li> <li>VM Disk Server(s) <ul> <li>Deals with in-game computer&#39;s storage, like floppies or hard drives.</li> </ul> </li> <li>Database server(s)</li> <li>Login server(s)</li> </ul> <p>All these servers need to exchange data, therefore the need for a flexible RPC framework.</p> <p>Initially I had a custom solution where I would tag methods of a class with certain attributes, then have a Clang based parser (<a href="https://github.com/Celtoys/clReflect">clReflect</a>) generate any required serialization and glue code.</p> <p>Although it worked fine for the most part, for the past year or so I kept wondering how could I use the new C++11/14 features to create a minimal type safe C++ RPC framework. Something that would not need a code generation step for glue code, while still keeping an acceptable API.</p> <p>For serialization of non-fundamental types, code generation is still useful, so I don&#39;t need to manually define how to serialize all the fields of a given struct/class. Although defining those manually is not a big deal, I believe.</p> <p><a name="id-rpc-parameters"></a></p> <h2>RPC Parameters</h2> <p>Given a function, in order to have type safe RPC calls, there are a few things we need to be able to do:</p> <ul> <li>Identify at compile time if this function is a valid RPC function (Right number of parameters, right type of parameters, etc)</li> <li>Check if the supplied parameters match (or can be converted) to what the function signature specifies.</li> <li>Serialize all parameters</li> <li>Deserialize all parameters</li> <li>Call the desired function</li> </ul> <p><a name="id-parameter-traits"></a></p> <h3>Parameter Traits</h3> <p>The first problem you&#39;ll face is in deciding what type of parameters are accepted. Some RPC frameworks only accept a limited number of types, such as Thrift. Let&#39;s check the problem.</p> <p>Given these function signatures:</p> <pre> <code class="language-cpp">void func1(int a, float b, char c); void func2(const char* a, std::string b, const std::string&amp; c); void func3(Foo a, Bar* b); </code></pre> <p>How can we make compile time checks regarding the parameters? Fundamental types are easy enough and should definitely be supported by the framework. A dumb memory copy will do the trick in those cases unless you want to trade a bit of performance for bandwidth usage by cutting down the number of bits needed. But how about complex types such std::string, std::vector, or your own classes? How about pointers, references, const references, rvalues?</p> <p>We can get some inspiration from what the C++ Standard Library does in the <a href="http://en.cppreference.com/w/cpp/types">type_traits</a> header. We need to be able to query a given type regarding its RPC properties. Let&#39;s put that concept in a template class <code>ParamTraits&lt;T&gt;</code>, with the following layout .</p> <table> <thead> <tr> <th><strong>Member constants</strong></th> <th>&nbsp;</th> </tr> </thead> <tbody> <tr> <td><code>valid</code></td> <td><code>true</code> if T is valid for RPC parameters, <code>false</code> otherwise</td> </tr> <tr> <td><strong>Member types</strong></td> <td>&nbsp;</td> </tr> <tr> <td><code>store_type</code></td> <td>Type used to hold the temporary copy needed when deserializing</td> </tr> <tr> <td><strong>Member functions</strong></td> <td>&nbsp;</td> </tr> <tr> <td><code>write</code></td> <td>Writes the parameter to a stream</td> </tr> <tr> <td><code>read</code></td> <td>Reads a parameter into a <code>store_type</code></td> </tr> <tr> <td><code>get</code></td> <td>Given a <code>store_type</code> parameter, it returns what can be passed to the RPC function as a parameter</td> </tr> </tbody> </table> <p>As an example, let&#39;s implement <code>ParamTraits&lt;T&gt;</code> for arithmetic types, considering we have a stream class with a <code>write</code> and <code>read</code> methods:</p> <pre> <code class="language-cpp">namespace cz { namespace rpc { // By default, all types for which ParamTraits is not specialized are invalid template &lt;typename T, typename ENABLED = void&gt; struct ParamTraits { using store_type = int; static constexpr bool valid = false; }; // Specialization for arithmetic types template &lt;typename T&gt; struct ParamTraits&lt; T, typename std::enable_if&lt;std::is_arithmetic&lt;T&gt;::value&gt;::type&gt; { using store_type = typename std::decay&lt;T&gt;::type; static constexpr bool valid = true; template &lt;typename S&gt; static void write(S&amp; s, typename std::decay&lt;T&gt;::type v) { s.write(&amp;v, sizeof(v)); } template &lt;typename S&gt; static void read(S&amp; s, store_type&amp; v) { s.read(&amp;v, sizeof(v)); } static store_type get(store_type v) { return v; } }; } // namespace rpc } // namespace cz </code></pre> <p>And a simple test:</p> <pre> <code class="language-cpp">#define TEST(exp) printf("%s = %s\n", #exp, exp ? "true" : "false"); void testArithmetic() { TEST(ParamTraits&lt;int&gt;::valid); // true TEST(ParamTraits&lt;const int&gt;::valid); // true TEST(ParamTraits&lt;int&amp;&gt;::valid); // false TEST(ParamTraits&lt;const int&amp;&gt;::valid); // false } </code></pre> <p><code>ParamTraits&lt;T&gt;</code> is also used to check if return types are valid, and since a void function is valid, we need to specialize <code>ParamTraits</code> for void too.</p> <pre> <code class="language-cpp">namespace cz { namespace rpc { // void type is valid template &lt;&gt; struct ParamTraits&lt;void&gt; { static constexpr bool valid = true; using store_type = void; }; } // namespace rpc } // namespace cz </code></pre> <p>The apparently strange thing with the specialization for <code>void</code> is that it also specifies a <code>store_type</code>. We can&#39;t use it to store anything, but will make some of the later template code easier.</p> <p>With these <code>ParamTraits</code> examples, references are not valid RPC parameters. In practice you do want to allow const references at least, especially for fundamental types. A tweak can be added to enable support for <code>const T&amp;</code> for any valid T if your application needs it.</p> <pre> <code class="language-cpp">// Make "const T&amp;" valid for any valid T #define CZRPC_ALLOW_CONST_LVALUE_REFS \ namespace cz { \ namespace rpc { \ template &lt;typename T&gt; \ struct ParamTraits&lt;const T&amp;&gt; : ParamTraits&lt;T&gt; { \ static_assert(ParamTraits&lt;T&gt;::valid, \ "Invalid RPC parameter type. Specialize ParamTraits if " \ "required."); \ }; \ } \ } </code></pre> <p>Similar tweaks can be made to enable support for <code>T&amp;</code> or <code>T&amp;&amp;</code> if required, although if the function makes changes to those parameters, those changes will be lost.</p> <p>Let&#39;s try adding support for a complex type, such as <code>std::vector&lt;T&gt;</code>. For <code>std::vector&lt;T&gt;</code> to be supported, <code>T</code> needs to be supported too.</p> <pre> <code class="language-cpp">namespace cz { namespace rpc { template &lt;typename T&gt; struct ParamTraits&lt;std::vector&lt;T&gt;&gt; { using store_type = std::vector&lt;T&gt;; static constexpr bool valid = ParamTraits&lt;T&gt;::valid; static_assert(ParamTraits&lt;T&gt;::valid == true, "T is not valid RPC parameter type."); // std::vector serialization is done by writing the vector size, followed by // each element template &lt;typename S&gt; static void write(S&amp; s, const std::vector&lt;T&gt;&amp; v) { int len = static_cast&lt;int&gt;(v.size()); s.write(&amp;len, sizeof(len)); for (auto&amp;&amp; i : v) ParamTraits&lt;T&gt;::write(s, i); } template &lt;typename S&gt; static void read(S&amp; s, std::vector&lt;T&gt;&amp; v) { int len; s.read(&amp;len, sizeof(len)); v.clear(); while (len--) { T i; ParamTraits&lt;T&gt;::read(s, i); v.push_back(std::move(i)); } } static std::vector&lt;T&gt;&amp;&amp; get(std::vector&lt;T&gt;&amp;&amp; v) { return std::move(v); } }; } // namespace rpc } // namespace cz // A simple test void testVector() { TEST(ParamTraits&lt;std::vector&lt;int&gt;&gt;::valid); // true // true if support for const refs was enabled TEST(ParamTraits&lt;const std::vector&lt;int&gt;&amp;&gt;::valid); }</code></pre> <p>For convenience, we can use the <code>&lt;&lt;</code> and <code>&gt;&gt;</code> operators with <code>Stream</code> class (not shown here). Those operators simply call the respective <code>ParamTraits&lt;T&gt;</code> <code>read</code> and <code>write</code> functions.</p> <p>Now that we can check if a specific type is allowed for RPC parameters, we can build on that and check if a function can be used for RPCs. This is implemented with variadic templates.</p> <p>First let&#39;s create a template to tells us if a bunch of parameters are valid.</p> <pre> <code class="language-cpp">namespace cz { namespace rpc { // // Validate if all
Modern C++ lightweight binary RPC framework without code generation
June 8, 2016

Tags:
Featured Blogs
Subscribe to our newsletter
About JikGuard.com
JikGuard.com, a high-tech security service provider focusing on game protection and anti-cheat, is committed to helping game companies solve the problem of cheats and hacks, and providing deeply integrated encryption protection solutions for games.
Top

test-AssetBundle Encryption: Powerful Unity Asset Protection Strategy
April 11, 2025

Game Lua Script Encryption: Ultimate Protection Guide for Developers
April 10, 2025

Unreal Engine Encryption: Safeguarding Game Assets and Communications
April 10, 2025

Game Lua Encryption: Safeguarding Your Game Scripts
April 9, 2025

Game Code Encryption: How to Protect Your Game from Reverse Engineering
April 9, 2025
Recent

test-AssetBundle Encryption: Powerful Unity Asset Protection Strategy
April 11, 2025

Game Lua Script Encryption: Ultimate Protection Guide for Developers
April 10, 2025

Unreal Engine Encryption: Safeguarding Game Assets and Communications
April 10, 2025

Game Lua Encryption: Safeguarding Your Game Scripts
April 9, 2025

Game Code Encryption: How to Protect Your Game from Reverse Engineering
April 9, 2025

Anti-Cheat FAQ: Best Anti-Cheat in Game for Ultimate Fair Play
April 9, 2025

How Windows Anti-Cheat Systems Are Winning the War Against Game Hackers
April 9, 2025

Nintendo is delaying Switch 2 pre-orders in Canada because of America
April 9, 2025

'We have way more power than any corporation:' Union workers call on developers to fight for their future
April 9, 2025

Testronic opens QA testing facility in Manila
April 9, 2025
Popular

Brass Lion Entertainment makes layoffs to 'realign' workforce around upcoming action-RPG
April 8, 2025

Aonic appoints former Larian UK boss Arthur Mostovoy as head of games
April 8, 2025

Twin Sails Interactive splits from Asmodee and Embracer to become an independent publisher
April 7, 2025

Obituary: Blizzard veteran and Overwatch art director Bill Petras has passed away
April 7, 2025

Nintendo taking 'variable' approach to pricing with Switch 2 titles
April 7, 2025

Nintendo delays Switch 2 pre-orders in the U.S. over Trump tariffs
April 5, 2025

Krafton brands life simulator InZoi a 'long-term franchise' after it hits 1 million sales
April 4, 2025

Best Anti Cheat Software: Ensuring Fair Play in Gaming
April 3, 2025

How Anti Cheating Systems Protect Online Games from Hackers
April 3, 2025

Anti-Hack Programs Explained: How They Keep Gaming Fair
April 3, 2025
Random

How Anti Cheating Systems Protect Online Games from Hackers
April 3, 2025

Nintendo taking 'variable' approach to pricing with Switch 2 titles
April 7, 2025

Unreal Engine Encryption: Safeguarding Game Assets and Communications
April 10, 2025

How Windows Anti-Cheat Systems Are Winning the War Against Game Hackers
April 9, 2025

Unity promises to deliver 'battle-tested' Switch 2 support
April 2, 2025

Amaze founder temporarily steps back after harassing event staff
March 31, 2025

Epic acquires AI-powered 3D asset tagging technology Loci
April 3, 2025

Game Code Encryption: How to Protect Your Game from Reverse Engineering
April 9, 2025

Embracer studio Eidos-Montreal has laid off 75 employees
April 1, 2025

Want to know the best way to gain attention on Steam? Add a demo.
April 1, 2025
Most Views

'Redesigned from the ground up:' The Nintendo Switch 2 will launch on June 5 for $449
April 2, 2025

Unity promises to deliver 'battle-tested' Switch 2 support
April 2, 2025

Want to know the best way to gain attention on Steam? Add a demo.
April 1, 2025

How Ninja Theory created Hellblade II's unsettling soundscape
April 1, 2025

Embracer studio Eidos-Montreal has laid off 75 employees
April 1, 2025

HYBE IM nets $21 million to expand publishing business by wielding the power of K-pop
April 1, 2025

Advanced Anti Speed Hack Solutions for Fair Gaming
March 31, 2025

Myst developer Cyan Worlds has laid off half of its workforce
March 31, 2025

Amaze founder temporarily steps back after harassing event staff
March 31, 2025

Monster Hunter Wilds has topped 10 million sales in one month
March 31, 2025