In the previous article about the declarative JSON serializer, I explained how using C ++ templates you can describe data structures and serialize them. This is very convenient, because not only reduces the size of the code, but also minimizes the number of possible errors. The concept - if the code is compiled, then it works. Approximately the same approach is applied in wjrpc, which will be discussed in this article. But since wjrpc is “torn out” from the framework under which it was designed for interfaces, I will also address the issues of architecture and asynchronous interfaces.
I will not describe the JSON serializer on which wjrpc runs and with which the JSON description is implemented for message data structures. I told about wjson in the previous article. Before considering the declarative version of the description of the API services for JSON-RPC, consider how you can implement parsing in “manually”. This will require writing more run-time code to extract data and checks, but it is easier to understand. All examples can be found in the examples section of the project.
As an example, consider the simple calculator API, which can perform addition, subtraction, multiplication, and division operations. The queries themselves are trivial, so I’ll only give the code for the addition operation:
#pragma once #include <memory> #include <functional> namespace request { struct plus { int first=0; int second=0; typedef std::unique_ptr<plus> ptr; }; } // request namespace response { struct plus { int value=0; typedef std::unique_ptr<plus> ptr; typedef std::function<void(ptr)> callback; }; } // response
I prefer the structure for each pair of request-response to place in a separate file in a specially designated folder. This makes it easier to navigate the project. Using namespaces and defining auxiliary types, as shown in the example above, is not necessary, but increases the readability of the code. The meaning of these typedefs will be explained later.
For each request, create a JSON description.
#pragma once #include "calc/api/plus.hpp" #include <wjson/json.hpp> #include <wjson/name.hpp> namespace request { struct plus_json { JSON_NAME(first) JSON_NAME(second) typedef wjson::object< plus, wjson::member_list< wjson::member<n_first, plus, int, &plus::first>, wjson::member<n_second, plus, int, &plus::second> > > type; typedef typename type::serializer serializer; typedef typename type::target target; }; } namespace response { struct plus_json { JSON_NAME(value) typedef wjson::object< plus, wjson::member_list< wjson::member<n_value, plus, int, &plus::value> > > type; typedef typename type::serializer serializer; typedef typename type::target target; }; }
Why put it in a structure, I told in the article on wjson. I will only note that here typedef definitions require that these structures be recognized as a json description.
Processing JSON-RPC messages can be divided into two stages. At the first stage, you need to determine the type of the request and the name of the method, and at the second, deserialize the parameters for this method.
<p>{ "jsonrpc":"2.0", "method":"plus", "params":{ "first":2, "second":3 }, "id":1 }</p> <source><!--</spoiler>--> , : <!--<spoiler title=" ">--> ```cpp struct request { std::string version, std::string method, std::string params, std::string id }
JSON_NAME(jsonrpc) JSON_NAME(method) JSON_NAME(params) JSON_NAME(id) typedef wjson::object< request, wjson::member_list< wjson::member<n_jsonrpc, request, std::string, &request::version>, wjson::member<n_method, request, std::string, &request::method>, wjson::member<n_params, request, std::string, &request::params, json::raw_value<> >, wjson::member<n_id, request, std::string, &request::id, json::raw_value<> > > > request_json;
With this description, in the request::params
and request::id
fields, json will be copied as is, without any conversion, and the actual name of the request::method
will be in the request::method
field. Having determined the name of the method, we can deserialize the parameters with the structures described above.
To determine the name of a method, it is not necessary to deserialize the entire query into an intermediate data structure. It is enough to parse it, and deserialize only the query piece belonging to the params field. This can be done using wjson :: parser directly, but wjson also provides the raw_pair construction (I did not consider it in the previous article), which will not deserialize the elements, but remember its location in the input buffer. Consider how this is implemented in wjrpc.
To begin with, wjrpc does not work with std :: string strings, but defines the following types:
namespace wjrpc { typedef std::vector<char> data_type; typedef std::unique_ptr<data_type> data_ptr; }
You can consider data_ptr as a relocatable buffer, the use of which allows us to guarantee that the data will not be copied over again.
Any incoming wjrpc message will attempt to deserialize into the following structure:
namespace wjrpc { struct incoming { typedef data_type::iterator iterator; typedef std::pair<iterator, iterator> pair_type; pair_type method; pair_type params; pair_type result; pair_type error; pair_type id; }; }
All wjrpc :: incoming elements are pairs of iterators in the input buffer. For example, method.first, when deserializing, will point to a quotation mark that opens the name of the method, after the colon, in the input request, and method.second to the next position after the closing quotation mark. This structure also describes not only the request, but also the response to the request as well as an error message. Determine the type of message simply by filling in the fields. JSON is the description for this structure:
namespace wjrpc { struct incoming_json { typedef incoming::pair_type pair_type; typedef wjson::iterator_pair<pair_type> pair_json; JSON_NAME(id) JSON_NAME(method) JSON_NAME(params) JSON_NAME(result) JSON_NAME(error) typedef wjson::object< incoming, wjson::member_list< wjson::member<n_method, incoming, pair_type, &incoming::method, pair_json>, wjson::member<n_params, incoming, pair_type, &incoming::params, pair_json>, wjson::member<n_result, incoming, pair_type, &incoming::result, pair_json>, wjson::member<n_error, incoming, pair_type, &incoming::error, pair_json>, wjson::member<n_id, incoming, pair_type, &incoming::id, pair_json> > > type; typedef type::target target; typedef type::member_list member_list; typedef type::serializer serializer; }; }
There is no full-fledged deserialization here - this is essentially parsing with remembering positions in the input buffer. Obviously, after such a deserialization, the data will be valid only as long as the input buffer exists.
The wjrpc :: incoming_holder class captures the buffer with the request and parses it into the structure described above. I will not describe in detail its interface, but I will show how you can use it to implement JSON-RPC.
#include "calc/api/plus.hpp" #include "calc/api/plus_json.hpp" #include <wjrpc/errors/error_json.hpp> #include <wjrpc/incoming/incoming_holder.hpp> #include <wjrpc/outgoing/outgoing_holder.hpp> #include <wjrpc/outgoing/outgoing_result.hpp> #include <wjrpc/outgoing/outgoing_result_json.hpp> #include <wjrpc/outgoing/outgoing_error.hpp> #include <wjrpc/outgoing/outgoing_error_json.hpp> #include <iostream> int main() { std::vector<std::string> req_list = { "{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }", "{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }", "{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }", "{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }" }; std::vector<std::string> res_list; for ( auto& sreq : req_list ) { wjrpc::incoming_holder inholder( sreq ); // inholder.parse(nullptr); // if ( inholder.method() == "plus" ) { // auto params = inholder.get_params<request::plus_json>(nullptr); // wjrpc::outgoing_result<response::plus> res; res.result = std::make_unique<response::plus>(); // res.result->value = params->first + params->second; // id auto raw_id = inholder.raw_id(); res.id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second ); // typedef wjrpc::outgoing_result_json<response::plus_json> result_json; res_list.push_back(std::string()); result_json::serializer()( res, std::back_inserter(res_list.back()) ); } /* else if ( inholder.method() == "minus" ) { ... } */ /* else if ( inholder.method() == "multiplies" ) { ... } */ /* else if ( inholder.method() == "divides" ) { ... } */ } for ( size_t i =0; i != res_list.size(); ++i) { std::cout << req_list[i] << std::endl; std::cout << res_list[i] << std::endl; std::cout << std::endl; } }
Here, most of the code is the formation of the answer, because we do not check for errors. First, we initialize the incoming_holder string and parse it. At this stage, the input string is deserialized into the incoming structure described above. If the string contains any valid json object, then this stage will pass without errors.
Next you need to determine the type of request. This is easily done by the presence or absence of the “method”, “result”, “error” and “id” fields.
Combination | Message type | Check | Receive |
---|---|---|---|
method and id | request | is_request | get_params <> |
method without id | notification | is_notify | get_params <> |
result and id | response to a request | is_response | get_result <> |
error and id | error in response to request | is_request_error | get_error <> |
error without id | other errors | is_other_error | get_error <> |
If none of the conditions are met, then it is obvious that the request is incorrect.
#include "calc/api/plus.hpp" #include "calc/api/plus_json.hpp" #include <wjrpc/errors/error_json.hpp> #include <wjrpc/incoming/incoming_holder.hpp> #include <wjrpc/outgoing/outgoing_holder.hpp> #include <wjrpc/outgoing/outgoing_result.hpp> #include <wjrpc/outgoing/outgoing_result_json.hpp> #include <wjrpc/outgoing/outgoing_error.hpp> #include <wjrpc/outgoing/outgoing_error_json.hpp> #include <iostream> int main() { std::vector<std::string> req_list = { "{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }", "{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }", "{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }", "{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }" }; std::vector<std::string> res_list; for ( auto& sreq : req_list ) { wjrpc::incoming_holder inholder( sreq ); wjson::json_error e; inholder.parse(&e); if ( e ) { typedef wjrpc::outgoing_error<wjrpc::error> error_type; error_type err; err.error = std::make_unique<wjrpc::parse_error>(); typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json; std::string str; error_json::serializer()(err, std::back_inserter(str)); res_list.push_back(str); } else if ( inholder.is_request() ) { auto raw_id = inholder.raw_id(); auto call_id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second ); // if ( inholder.method() == "plus" ) { auto params = inholder.get_params<request::plus_json>(&e); if ( !e ) { wjrpc::outgoing_result<response::plus> res; res.result = std::make_unique<response::plus>(); res.result->value = params->first + params->second; res.id = std::move(call_id); typedef wjrpc::outgoing_result_json<response::plus_json> result_json; std::string str; result_json::serializer()( res, std::back_inserter(str) ); res_list.push_back(str); } else { typedef wjrpc::outgoing_error<wjrpc::error> error_type; error_type err; err.error = std::make_unique<wjrpc::invalid_params>(); err.id = std::move(call_id); typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json; std::string str; error_json::serializer()(err, std::back_inserter(str)); res_list.push_back(str); } } /* else if ( inholder.method() == "minus" ) { ... } */ /* else if ( inholder.method() == "multiplies" ) { ... } */ /* else if ( inholder.method() == "divides" ) { ... } */ else { typedef wjrpc::outgoing_error<wjrpc::error> error_type; error_type err; err.error = std::make_unique<wjrpc::procedure_not_found>(); err.id = std::move(call_id); typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json; std::string str; error_json::serializer()(err, std::back_inserter(str)); res_list.push_back(str); } } else { typedef wjrpc::outgoing_error<wjrpc::error> error_type; error_type err; err.error = std::make_unique<wjrpc::invalid_request>(); typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json; std::string str; error_json::serializer()(err, std::back_inserter(str)); res_list.push_back(str); } } for ( size_t i =0; i != res_list.size(); ++i) { std::cout << req_list[i] << std::endl; std::cout << res_list[i] << std::endl; std::cout << std::endl; } }
Here, most of the code is error handling, or rather, the formation of the corresponding message. But for all types of errors, the code is similar, the differences are only in the type of error. You can make one template function for serializing all types of errors.
template<typename E> void make_error(wjrpc::incoming_holder inholder, std::string& out) { typedef wjrpc::outgoing_error<wjrpc::error> common_error; common_error err; err.error = std::make_unique<E>(); if ( inholder.has_id() ) { auto id = inholder.raw_id(); err.id = std::make_unique<wjrpc::data_type>(id.first, id.second); } typedef wjrpc::outgoing_error_json<wjrpc::error_json> error_json; error_json::serializer()(err, std::back_inserter(out)); }
To generate an error message, we only need to know the type of error and the call identifier, if any. The inholder object is moved and after the formation of the message it is no longer needed. In the example, it is used only to retrieve the call identifier, but it can also “pick up” the input buffer — to serialize messages there so as not to create a new one.
Similarly, it is possible to implement the result serialization. But before continuing to get rid of the same type of code, let's put in order the application part, which is somewhat lost here and is represented with just one line for each method.
As I said, wjrpc is torn out of the framework, in which the interface must be explicitly defined for the components. Moreover, it is not just a structure exclusively with pure virtual methods, but also certain restrictions are imposed on the parameters of the methods.
All input and output parameters should be combined into structures, even if there is only one field. This is convenient not only for the formation of a json description for a query, when the deserialized structure can be passed directly to the method, without prior conversion, but also from the standpoint of extensibility.
For example, in all methods we return a number - the result of the operation. Meaning to describe the result structure with one field, if possible number? And the input parameters in general could be passed by an array (positional parameters).
But now imagine that the requirements have changed a bit, and to the list of parameters you need to add supporting information, or, in the answer, for the division operation, a flag indicating that division by zero has occurred. In this case, to make corrections, you need to “shake up” not only the interface and all its implementations, but also all the places where it is used.
If we have a JSON-RPC service, then the changes will affect not only the server, but also the client, whose interfaces need to be somehow coordinated. And in the case of structures, it is enough just to add a field to the structure. Moreover, you can update the client part and the server part independently.
Another feature of the framework is that all methods have an asynchronous interface, i.e. the result is not returned directly, but through a callback function. And in order to avoid unintended copying errors, the input and output objects are described as std :: unique_ptr <>.
For our calculator, taking into account the described limitations, we get the following interface:
struct icalc { virtual ~icalc() {} virtual void plus( request::plus::ptr req, response::plus::callback cb) = 0; virtual void minus( request::minus::ptr req, response::minus::callback cb) = 0; virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) = 0; virtual void divides( request::divides::ptr req, response::divides::callback cb) = 0; };
Given the auxiliary typedefs that we defined in the input structures, it looks quite nice. But the implementation for such interfaces can be quite voluminous with respect to simple examples. You need to make sure that the input request is not nullptr, and also check the callback function. If it is not defined, then it is obvious that this is a notification, and in this case, the call must simply be ignored. This functionality is easy to template, as shown in the example implementation of our calculator
#pragma once #include "icalc.hpp" class calc1 : public icalc { public: virtual void plus( request::plus::ptr req, response::plus::callback cb) override; virtual void minus( request::minus::ptr req, response::minus::callback cb) override; virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override; virtual void divides( request::divides::ptr req, response::divides::callback cb) override; private: template<typename Res, typename ReqPtr, typename Callback, typename F> void impl_( ReqPtr req, Callback cb, F f); };
#include "calc1.hpp" #include <wjrpc/memory.hpp> template<typename Res, typename ReqPtr, typename Callback, typename F> void calc1::impl_( ReqPtr req, Callback cb, F f) { // if ( cb == nullptr ) return; // if ( req == nullptr ) return cb(nullptr); auto res = std::make_unique<Res>(); res->value = f(req->first,req->second); cb( std::move(res) ); } void calc1::plus( request::plus::ptr req, response::plus::callback cb) { this->impl_<response::plus>( std::move(req), cb, [](int f, int s) { return f+s; } ); } void calc1::minus( request::minus::ptr req, response::minus::callback cb) { this->impl_<response::minus>( std::move(req), cb, [](int f, int s) { return fs; }); } void calc1::multiplies( request::multiplies::ptr req, response::multiplies::callback cb) { this->impl_<response::multiplies>( std::move(req), cb, [](int f, int s) { return f*s; }); } void calc1::divides( request::divides::ptr req, response::divides::callback cb) { this->impl_<response::divides>( std::move(req), cb, [](int f, int s) { return s!=0 ? f/s : 0; }); }
The article is not about the implementation of the calculator, so the code presented above should not be taken as best practice. An example of a method call:
calc->plus( std::move(params), [](response::plus::ptr result) { … });
Back to the topic of interfaces, and now we will continue to “shorten” the code of our service. In order to asynchronously serialize the result, it is necessary to “capture” the incoming request.
std::shared_ptr<wjrpc::incoming_holder> ph = std::make_shared<wjrpc::incoming_holder>( std::move(inholder) ); calc->plus( std::move(params), [ph, &res_list](response::plus::ptr result) { // wjrpc::outgoing_result<response::plus> resp; resp.result = std::move(result); // auto raw_id = ph->raw_id(); auto call_id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second ); resp.id = std::move(call_id); // typedef wjrpc::outgoing_result_json<response::plus_json> result_json; typedef wjrpc::outgoing_result_json<response::plus_json> result_json; // auto d = ph->detach(); d->clear(); result_json::serializer()( resp, std::back_inserter(d) ); res_list.push_back( std::string(d->begin(), d->end()) ); });
Since incoming_holder is moved, then in order to “capture” it, move it to std :: shared_ptr. It shows how you can take a buffer from him, but in this case it does not make much sense - all the same, the result is placed in the list of strings. Capturing res_list by reference is for example only. we know that the request will be executed synchronously.
We have already written a template function for serializing errors, we will do the same for answers. But for this, in addition to the type of result, you need to pass its value and json-description.
template<typename ResJ> void send_response(std::shared_ptr<wjrpc::incoming_holder> ph, typename ResJ::target::ptr result, std::string& out) { typedef ResJ result_json; typedef typename result_json::target result_type; wjrpc::outgoing_result<result_type> resp; resp.result = std::move(result); auto raw_id = ph->raw_id(); resp.id = std::make_unique<wjrpc::data_type>( raw_id.first, raw_id.second ); typedef wjrpc::outgoing_result_json<result_json> response_json; typename response_json::serializer()( resp, std::back_inserter( out ) ); }
Here you need to specify the template parameter explicitly - this is the json description for the response structure, from which you can take the type of the described structure. Using this function, the code for each JSON-RPC method will be greatly simplified.
if ( inholder.method() == "plus" ) { // auto params = inholder.get_params<request::plus_json>(&e); if ( !e ) { std::shared_ptr<wjrpc::incoming_holder> ph = std::make_shared<wjrpc::incoming_holder>( std::move(inholder) ); calc->plus( std::move(params), std::bind( send_response<response::plus_json>, ph, std::placeholders::_1, std::ref(out)) ); } else { make_error<wjrpc::invalid_params>(std::move(inholder), out ); } } // else if ( inholder.method() == "minus" ) { ... } // else if ( inholder.method() == "multiplies" ) { .... } // else if ( inholder.method() == "divides" ) { .... } else { make_error<wjrpc::procedure_not_found>(std::move(inholder), out ); }
For each method, the code was reduced to a minimum, but this is not enough for me. Receiving parameters and checking for an error is also the same type code.
template< typename JParams, typename JResult, void (icalc::*mem_ptr)( std::unique_ptr<typename JParams::target>, std::function< void(std::unique_ptr<typename JResult::target>) > ) > void invoke(wjrpc::incoming_holder inholder, std::shared_ptr<icalc> calc, std::string& out) { typedef JParams params_json; typedef JResult result_json; wjson::json_error e; auto params = inholder.get_params<params_json>(&e); if ( !e ) { std::shared_ptr<wjrpc::incoming_holder> ph = std::make_shared<wjrpc::incoming_holder>( std::move(inholder) ); (calc.get()->*mem_ptr)( std::move(params), std::bind( send_response<result_json>, ph, std::placeholders::_1, std::ref(out) ) ); } else { out = make_error<wjrpc::invalid_params>(); } }
This is almost the same as implemented in wjrpc. As a result, the code of the demo example will be reduced to a minimum (here you can bring the implementation of all methods)
int main() { std::vector<std::string> req_list = { "{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }", "{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }", "{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }", "{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }" }; std::vector<std::string> res_list; auto calc = std::make_shared<calc1>(); for ( auto& sreq : req_list ) { res_list.push_back( std::string() ); std::string& out = res_list.back(); wjrpc::incoming_holder inholder( sreq ); wjson::json_error e; inholder.parse(&e); if ( e ) { out = make_error<wjrpc::parse_error>(); } else if ( inholder.is_request() ) { // if ( inholder.method() == "plus" ) { invoke<request::plus_json, response::plus_json, &icalc::plus>( std::move(inholder), calc, out ); } else if ( inholder.method() == "minus" ) { invoke<request::minus_json, response::minus_json, &icalc::minus>( std::move(inholder), calc, out ); } else if ( inholder.method() == "multiplies" ) { invoke<request::multiplies_json, response::multiplies_json, &icalc::multiplies>( std::move(inholder), calc, out ); } else if ( inholder.method() == "divides" ) { invoke<request::divides_json, response::divides_json, &icalc::divides>( std::move(inholder), calc, out ); } else { out = make_error<wjrpc::procedure_not_found>(); } } else { out = make_error<wjrpc::invalid_request>(); } } for ( size_t i =0; i != res_list.size(); ++i) { std::cout << req_list[i] << std::endl; std::cout << res_list[i] << std::endl; std::cout << std::endl; } }
Such a manic desire to reduce the run-time amount of code due to several reasons. Removing the extra if
using a rather complex construction, we not only reduce the amount of code, but also remove potential places where the programmer will be happy to commit to code. And programmers love to copy-paste, especially uninteresting code associated with serialization, spreading it across the entire project, or even adding it to other projects. Laziness is the engine of progress when it makes a person invent something that will allow him to work less. But not in the case when things are postponed until later. It would seem a trivial check, but with words - this is still a prototype, writing a couple of lines of code is postponed, then forgotten and at the same time copied to the entire project, and also picked up by other programmers.
In fact, we have not yet begun to consider wjrpc. , wjson, , , JSON-RPC , , . wjrpc:
#include "calc/calc1.hpp" #include "calc/api/plus_json.hpp" #include "calc/api/minus_json.hpp" #include "calc/api/multiplies_json.hpp" #include "calc/api/divides_json.hpp" #include <wjrpc/handler.hpp> #include <wjrpc/method.hpp> #include <iostream> #include <functional> JSONRPC_TAG(plus) JSONRPC_TAG(minus) JSONRPC_TAG(multiplies) JSONRPC_TAG(divides) struct method_list: wjrpc::method_list < wjrpc::target<icalc>, wjrpc::invoke_method<_plus_, request::plus_json, response::plus_json, icalc, &icalc::plus>, wjrpc::invoke_method<_minus_, request::minus_json, response::minus_json, icalc, &icalc::minus>, wjrpc::invoke_method<_multiplies_, request::multiplies_json, response::multiplies_json, icalc, &icalc::multiplies>, wjrpc::invoke_method<_divides_, request::divides_json, response::divides_json, icalc, &icalc::divides> >{}; class handler: public wjrpc::handler<method_list> {}; int main() { std::vector<std::string> req_list = { "{\"method\":\"plus\", \"params\":{ \"first\":2, \"second\":3 }, \"id\" :1 }", "{\"method\":\"minus\", \"params\":{ \"first\":5, \"second\":10 }, \"id\" :1 }", "{\"method\":\"multiplies\", \"params\":{ \"first\":2, \"second\":2 }, \"id\" :1 }", "{\"method\":\"divides\", \"params\":{ \"first\":9, \"second\":3 }, \"id\" :1 }" }; std::vector<std::string> res_list; auto calc = std::make_shared<calc1>(); handler h; handler::options_type opt; opt.target = calc; h.start(opt, 1); for ( auto& sreq : req_list ) { h.perform( sreq, [&res_list](std::string out) { res_list.push_back(out);} ); } for ( size_t i =0; i != res_list.size(); ++i) { std::cout << req_list[i] << std::endl; std::cout << res_list[i] << std::endl; std::cout << std::endl; } }
JSONRPC_TAG , JSON NAME wjson, , n , .
wjrpc::method_list wjrpc::invoke_method . wjrpc::handler. , .
perform_io, wjrpc::data_ptr.
typedef std::vector<char> data_type; typedef std::unique_ptr<data_type> data_ptr; typedef std::function< void(data_ptr) > output_handler_t; void perform_io(data_ptr d, output_handler_t handler) { … } void perform(std::string str, std::function<void(std::string)> handler) { auto d = std::make_unique<data_type>( str.begin(), str.end() ); this->perform_io( std::move(d), [handler](data_ptr d) { handler( std::string(d->begin(), d->end()) ); }); }
, . , , ,
struct plus_handler { template<typename T> void operator()(T& t, request::plus::ptr req) { // t.target()->plus( std::move(req), nullptr ); } template<typename T, typename Handler> void operator()(T& t, request::plus::ptr req, Handler handler) { // t.target()->plus( std::move(req), [handler](response::plus::ptr res) { if ( res != nullptr ) handler( std::move(res), nullptr ); else handler( nullptr, std::make_unique<wjrpc::service_unavailable>() ); }); } };
struct plus_handler { template<typename T> void operator()(T&, request::plus::ptr req) { } template<typename T, typename Handler> void operator()(T&, request::plus::ptr req, Handler handler) { if (req==nullptr) { handler( nullptr, std::make_unique<wjrpc::invalid_params>() ); return; } auto res = std::make_unique<response::plus>(); res->value = req->first + req->second; handler( std::move(res), nullptr ); } };
handler , , . t — JSON-RPC- ( self python). :
struct method_list: wjrpc::method_list < wjrpc::target<icalc>, wjrpc::method< wjrpc::name<_plus_>, wjrpc::invoke<request::plus_json, response::plus_json, plus_handler> >, wjrpc::invoke_method<_minus_, request::minus_json, response::minus_json, icalc, &icalc::minus>, wjrpc::invoke_method<_multiplies_, request::multiplies_json, response::multiplies_json, icalc, &icalc::multiplies>, wjrpc::invoke_method<_divides_, request::divides_json, response::divides_json, icalc, &icalc::divides> >{};
, invoke_method<> wjrpc::method<>, :
template< typename Params, typename Result, typename I, void (I::*mem_ptr)( std::unique_ptr<Params>, std::function< void(std::unique_ptr<Result>) > ) > struct mem_fun_handler { typedef std::unique_ptr<Params> request_ptr; typedef std::unique_ptr<Result> responce_ptr; typedef std::unique_ptr< error> json_error_ptr; typedef std::function< void(responce_ptr, json_error_ptr) > jsonrpc_callback; template<typename T> void operator()(T& t, request_ptr req) const; template<typename T> void operator()(T& t, request_ptr req, jsonrpc_callback cb) const; };
, JSON-RPC , , .
, ( ), , ( ) . . , - , . . , ? - . , , . , , . , , , .
- “” . wjrpc::invoke_method. , JSON-RPC. “” , .
, wjrpc::invoke_method.
struct icalc { virtual ~icalc() {} virtual request::plus::ptr plus( request::plus::ptr req) = 0; virtual request::minus::ptr minus( request::minus::ptr req) = 0; virtual request::multiplies::ptr multiplies( request::multiplies::ptr req) = 0; virtual request::divides::ptr divides( request::divides::ptr req) = 0; };
wjrpc::handler<> . callback, , , , , .
calc->plus( std::move(req), [this](response::plus::ptr) {} );
std::shared_ptr<calc> pthis = this->shared_from_this(); calc->plus( std::move(req), [pthis](response::plus::ptr) {} );
, .. . Option
std::weak_ptr<calc> wthis = this->shared_from_this(); calc->plus( std::move(req), [wthis](response::plus::ptr) { if ( auto pthis = wthis.lock() ) { /* … */ } } );
, (, ). , , callback- .
std::weak_ptr<int> w = this->_p; /* _p = std::shared_ptr<int>(1);*/ std::weak_ptr<calc> wthis = this->shared_from_this(); calc->plus( std::move(req), [wthis, w](response::plus::ptr) { if ( auto pthis = wthis.lock() ) { if ( nullptr == w.lock() ) return; /* … */ } } );
( wjrpc)
template<typename H> class owner_handler { public: typedef std::weak_ptr<int> weak_type; owner_handler() = default; owner_handler(H&& h, weak_type alive) : _handler( std::forward<H>(h) ) , _alive(alive) { } template <class... Args> auto operator()(Args&&... args) -> typename std::result_of< H(Args&&...) >::type { if ( auto p = _alive.lock() ) { return _handler( std::forward<Args>(args)... ); } return typename std::result_of< H(Args&&...) >::type(); } private: H _handler; weak_type _alive; };
class owner { public: typedef std::shared_ptr<int> alive_type; typedef std::weak_ptr<int> weak_type; owner() : _alive( std::make_shared<int>(1) ) { } owner(const owner& ) = delete; owner& operator = (const owner& ) = delete; owner(owner&& ) = default; owner& operator = (owner&& ) = default; alive_type& alive() { return _alive; } const alive_type& alive() const { return _alive; } void reset() { _alive = std::make_shared<int>(*_alive + 1); } template<typename Handler> owner_handler<typename std::remove_reference<Handler>::type> wrap(Handler&& h) const { return owner_handler< typename std::remove_reference<Handler>::type >( std::forward<Handler>(h), std::weak_ptr<int>(_alive) ); } private: mutable alive_type _alive; };
// owner - std::weak_ptr<calc> wthis = this->shared_from_this(); calc->plus( std::move(req), this->wrap([wthis](response::plus::ptr) { if ( auto pthis = wthis.lock() ) { /* … */ } })); // … // owner::reset(); // callback-
. . , . , , . , .
wjrpc::engine – , , jsonrpc-, . , , , wjrpc::engine. . callback-, , . wjrpc::engine, , , , .
, icalc. , plus, .
#pragma once #include "icalc.hpp" class calc_p : public icalc { public: void initialize(std::shared_ptr<icalc>); virtual void plus( request::plus::ptr req, response::plus::callback cb) override; virtual void minus( request::minus::ptr req, response::minus::callback cb) override; virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override; virtual void divides( request::divides::ptr req, response::divides::callback cb) override; private: template<typename ReqPtr, typename Callback> bool check_( ReqPtr& req, Callback& cb); std::shared_ptr<icalc> _next; };
#include "calc_p.hpp" #include <memory> void calc_p::initialize(std::shared_ptr<icalc> next) { _next = next; } void calc_p::plus( request::plus::ptr req, response::plus::callback cb) { if ( !this->check_(req, cb)) return; req->first++; req->second++; _next->plus(std::move(req), [cb](response::plus::ptr res) { res->value++; cb(std::move(res) ); }); } void calc_p::minus( request::minus::ptr req, response::minus::callback cb) { if ( this->check_(req, cb)) _next->minus(std::move(req), std::move(cb) ); } void calc_p::multiplies( request::multiplies::ptr req, response::multiplies::callback cb) { if ( this->check_(req, cb)) _next->multiplies(std::move(req), std::move(cb) ); } void calc_p::divides( request::divides::ptr req, response::divides::callback cb) { if ( this->check_(req, cb)) _next->divides(std::move(req), std::move(cb) ); } template<typename ReqPtr, typename Callback> bool calc_p::check_( ReqPtr& req, Callback& cb) { if ( cb==nullptr ) return false; if ( req != nullptr ) return true; cb(nullptr); return false; }
, icalc, , plus, “”. .
, : , , . , , .
, , , JSON-RPC .
JSONRPC_TAG(plus) JSONRPC_TAG(minus) JSONRPC_TAG(multiplies) JSONRPC_TAG(divides) struct method_list: wjrpc::method_list < wjrpc::call_method<_plus_, request::plus_json, response::plus_json>, wjrpc::call_method<_minus_, request::minus_json, response::minus_json>, wjrpc::call_method<_multiplies_, request::multiplies_json, response::multiplies_json>, wjrpc::call_method<_divides_, request::divides_json, response::divides_json, icalc> > {};
, , , JSON- . — icalc:
class handler : public ::wjrpc::handler<method_list> , public icalc { public: virtual void plus( request::plus::ptr req, response::plus::callback cb) override { this->template call<_plus_>( std::move(req), cb, nullptr ); } virtual void minus( request::minus::ptr req, response::minus::callback cb) override { this->template call<_minus_>( std::move(req), cb, nullptr ); } virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override { this->template call<_multiplies_>( std::move(req), cb, nullptr ); } virtual void divides( request::divides::ptr req, response::divides::callback cb) override { this->template call<_divides_>( std::move(req), cb, nullptr ); } };
— call<> , , , . , . , callback nullptr.
JSON-RPC , callback nullptr, , , . JSON-RPC . , .
, , :
, , , , . , , , . , . — , . , , , .
. callback, callback, , . JSON-RPC , id JSON-RPC . callback , callback, .
#include "calc/calc1.hpp" #include "calc/calc_p.hpp" #include "calc/api/plus_json.hpp" #include "calc/api/minus_json.hpp" #include "calc/api/multiplies_json.hpp" #include "calc/api/divides_json.hpp" #include <wjrpc/engine.hpp> #include <wjrpc/handler.hpp> #include <wjrpc/method.hpp> #include <iostream> #include <functional> namespace service { JSONRPC_TAG(plus) JSONRPC_TAG(minus) JSONRPC_TAG(multiplies) JSONRPC_TAG(divides) struct method_list: wjrpc::method_list < wjrpc::target<icalc>, wjrpc::invoke_method<_plus_, request::plus_json, response::plus_json, icalc, &icalc::plus>, wjrpc::invoke_method<_minus_, request::minus_json, response::minus_json, icalc, &icalc::minus>, wjrpc::invoke_method<_multiplies_, request::multiplies_json, response::multiplies_json, icalc, &icalc::multiplies>, wjrpc::invoke_method<_divides_, request::divides_json, response::divides_json, icalc, &icalc::divides> >{}; class handler: public ::wjrpc::handler<method_list> {}; typedef wjrpc::engine<handler> engine_type; } namespace gateway { JSONRPC_TAG(plus) JSONRPC_TAG(minus) JSONRPC_TAG(multiplies) JSONRPC_TAG(divides) struct method_list: wjrpc::method_list < wjrpc::call_method<_plus_, request::plus_json, response::plus_json>, wjrpc::call_method<_minus_, request::minus_json, response::minus_json>, wjrpc::call_method<_multiplies_, request::multiplies_json, response::multiplies_json>, wjrpc::call_method<_divides_, request::divides_json, response::divides_json> > {}; class handler : public ::wjrpc::handler<method_list> , public icalc { public: virtual void plus( request::plus::ptr req, response::plus::callback cb) override { this->template call<_plus_>( std::move(req), cb, nullptr ); } virtual void minus( request::minus::ptr req, response::minus::callback cb) override { this->template call<_minus_>( std::move(req), cb, nullptr ); } virtual void multiplies( request::multiplies::ptr req, response::multiplies::callback cb) override { this->template call<_multiplies_>( std::move(req), cb, nullptr ); } virtual void divides( request::divides::ptr req, response::divides::callback cb) override { this->template call<_divides_>( std::move(req), cb, nullptr ); } }; typedef wjrpc::engine<handler> engine_type; } int main() { // N1 auto prx1 = std::make_shared<calc_p>(); // auto gtw = std::make_shared<gateway::engine_type>(); // auto srv = std::make_shared<service::engine_type>(); // N2 auto prx2 = std::make_shared<calc_p>(); // auto clc = std::make_shared<calc1>(); // prx2->initialize(clc); // service::engine_type::options_type srv_opt; srv_opt.target = prx2; srv->start(srv_opt, 11); // gateway::engine_type::options_type cli_opt; gtw->start(cli_opt, 22); // gtw->reg_io(33, [srv]( wjrpc::data_ptr d, wjrpc::io_id_t /*io_id*/, wjrpc::output_handler_t handler) { std::cout << " REQUEST: " << std::string( d->begin(), d->end() ) << std::endl; srv->perform_io(std::move(d), 44, [handler](wjrpc::data_ptr d) { // JSON-RPC std::cout << " RESPONSE: " << std::string( d->begin(), d->end() ) << std::endl; handler(std::move(d) ); }); }); // ID auto gtwh = gtw->find(33); // prx1->initialize(gtwh); // plus (prx1->gtw->srv->prx2->clc) auto plus = std::make_unique<request::plus>(); plus->first = 1; plus->second = 2; prx1->plus( std::move(plus), [](response::plus::ptr res) { std::cout << "1+2=" << res->value << std::endl;; }); // plus (gtw->srv->prx2->clc) auto minus = std::make_unique<request::minus>(); minus->first = 4; minus->second = 3; gtwh->minus( std::move(minus), [](response::minus::ptr res) { std::cout << "4-3=" << res->value << std::endl;; }); }
Result:
REQUEST: {"jsonrpc":"2.0","method":"plus","params":{"first":2,"second":3},"id":1} RESPONSE: {"jsonrpc":"2.0","result":{"value":8},"id":1} 1+2=9 REQUEST: {"jsonrpc":"2.0","method":"minus","params":{"first":4,"second":3},"id":2} RESPONSE: {"jsonrpc":"2.0","result":{"value":1},"id":2} 4-3=1
, JSON-RPC 2 3 1 2. , , 8. , 9. , , , munus , .
— , , , , , , :
inline wjrpc::io_id_t create_id() { static std::atomic<wjrpc::io_id_t> counter( (wjrpc::io_id_t(1)) ); return counter.fetch_add(1); }
. , :
gtw->reg_io(33, []( wjrpc::data_ptr, wjrpc::io_id_t, wjrpc::output_handler_t)
JSON-RPC , . - , , , . . . 44 — , . service::engine_type, ( , wjrpc ), .
(::pipe), , , , , - , , , . examples , , , .
wjson, , wjrpc::incoming_holder JSON-RPC . , wjrpc::handler , run-time .
git clone https://github.com/migashko/faslib.git git clone https://github.com/mambaru/wjson.git git clone https://github.com/mambaru/wjrpc.git # wjrpc cd faslib mkdir build cd build cmake .. # cd ../../wjrpc mkdir build cd build cmake -DWJRPC_BUILD_ALL=ON .. make
Source: https://habr.com/ru/post/312994/
All Articles