📜 ⬆️ ⬇️

RMI means C ++ and boost.preprocessor

This is my first publication on this resource, therefore, I ask you to treat with understanding the mistakes I made.

RMI is a very commonplace task for PLs supporting introspection . But, C ++, unfortunately, does not apply to them.

In this publication I want to demonstrate the possibility of implementing a very usable RMI using C ++ preprocessor.
')

Formulation of the problem


1. Provide as simple a syntax as possible so that it is impossible to make a mistake.
2. Identification (binding) of procedures must be hidden from the user in order to make it impossible to make a mistake.
3. Syntax should not impose restrictions on the used C ++ types.
4. There should be the possibility of versioning procedures, but, so that compatibility with existing clients does not break.

Regarding the fourth point:
For example, we already have two add / div procedures, one version of each. We want to add add new version. If you just add another version, we’ll have the IDs of the procedures, which are known to the client programs collected before making this change.

Tool selection


Since The end result is supposed to be used in conjunction with C ++ code. I see three options:


I will speak about each of the options, respectively:


Regarding the first, second and third paragraphs of the requirements - the preprocessor version is suitable.

So, the choice is made - we use a preprocessor. And yes, of course boost.preprocessor .

Little about the preprocessor


Data types With ++ preprocessor:

Types, as you can see, more than enough.
A little thought, reading about the possibilities and limitations of each of them, as well as taking into account the desired simplicity of the syntax and the inability to make a mistake - the choice was made in favor of sequences and tuples.

A few explanatory examples.
(a)(b)(c) - sequence. Here, we described a sequence consisting of three elements.
(a) - also sequence, but consisting of one element. (Attention!)
(a)(b, c)(d, e, f) - again the sequence, but consisting of three tuples. (pay attention to the first element - the trick, however, but this is really a tuple)
(a)(b, c)(d, (e, f)) - again sequence, and also consisting of three tuples. But! The last tuple consists of two elements: 1) any element, 2) tuple.
And, finally, such an example: (a)(b, c)(d, (e, (f)(g))) - then figure it out yourself;)
As you can see, everything is really unreal simple.

We prototype syntax


After a brief reflection, the following syntax appears:
 (proc_name0, //   (signature_arg0, signature_arg1, signature_argN) //    (signature_arg0) //    ) (proc_name1, //   (signature_arg0, signature_arg1) //     ) (proc_name2, //   () //     ( ) ) 

Well ... quite useful, however.

Some implementation details


Since One of the requirements is the versioning of the procedures, and even such that compatibility with existing customers does not break - we will need two ID `s to identify the procedures. The first is the procedure ID, the second is the version ID.

Let me explain by example.
Suppose this is a description of the API of our service. Suppose we already have client programs using this 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 ) 

Now, for proc_name0() we need to add another version with a different signature.
 (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 ) 

Thus, we have a new ID version of the procedure, while the former remained unchanged.
It was: (0: 0), it became: (0: 0) (0: 1)
Those. this is what we tried to achieve. Previous clients both used (0: 0) and will continue to use these identifiers without worrying about the new versions of these procedures.
We also agree that all new procedures should be added to the end.

Next, we need to make sure that ID `s automatically put on both sides of the service. Easily! - just use the same described sequence twice, to generate client and server sides!

It's time to imagine how we want to see all this in the end:
 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 )) ) 

So that there is no confusion as to who is the leader and who is the slave - we agree that the procedures described on one of the parties are implementations on the opposite side. Ie, for example, client_invoker::registration(std::string, std::string) tells us that the implementation of this procedure will be on the server side, while the interface to this procedure will be on the side customer and vice versa.
(we use double parentheses because the preprocessor, when forming the argument for our MACRO (), will deploy the API described by us. This can be overcome, but I don’t know whether it is necessary? ..)

Total


From the above macro call, the code will be generated, which is under the spoiler.
Code
 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 


(another project is used as serialization - YAS )

As a bonus, the system procedure yarmi_error() was added - used to inform the opposite party that an error occurred while trying to make a call. Look carefully, client_invoker::invoke() , deserialization and call are wrapped in a try{}catch() block, and in catch() blocks a call is made to yarmi_error() . Thus, if an exception is raised during deserialization or a procedure call, it will be successfully intercepted by the catch() block, and the exception information will be sent to the caller. The same thing will happen in the opposite direction. Those. If the server has called the client for the procedure that caused the exception during the call, the client will send the error information to the server, also giving the ID and the version of the call that caused the exception. But, you can use yarmi_error() yourself, nothing prohibits this. Example: yarmi_error(call_id, version_id, "message");

As you can see, on the side of their implementation, the prefix on_

The client_invoker and server_invoker take two parameters. Their first bottom is the class in which the callable procedures are implemented, the second is the class in which the send(yas::shared_buffer buf) method send(yas::shared_buffer buf) implemented.
If you have the same class performs both roles, you can do 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) // <<<<<<<<<<<<<<<<<<<<<<<<< {} }; 

The final version looks like 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) {} }; 


The interface to the opposite side will be inherited from the ancestor of yarmi::client_invoker . Ie, for example, being in the constructor of our client_session , you can call the registration() procedure as follows:
 { registration("niXman"); } 

We will get the answer in our implementation client_session::on_registration(std::string msg, std::string regkey)
Everything!

Of the shortcomings should note the following:
Commas cannot be used in type names that describe procedures, because the preprocessor does not understand the context in which they are used. Will be fixed.

Ultimately, all of this resulted in a project called YARMI (Yet Another RMI).
The described code generator is encoded in one file - yarmi.hpp . In total, the implementation of the code generator took one working day.

An example of using this whole thing can be seen here and here . The first test project is still not completed, unfortunately.

In addition to the described, on the project page you will find the codes of the asynchronous multi-user single-threaded server, and the codes of the client.

Instead of conclusion


Plans:
1. Generating multiple interfaces
2. Describe the specification (although it is nowhere easier)
3. The ability to use your own allocator

I would be grateful for constructive criticism and suggestions.

PS
This code is used in several of our commercial projects, in game dev.

Source: https://habr.com/ru/post/199496/


All Articles