diff options
Diffstat (limited to 'modules/remote')
| -rw-r--r-- | modules/remote/c++/remote.hpp | 56 | ||||
| -rw-r--r-- | modules/remote/c++/remote_loopback.hpp | 113 | ||||
| -rw-r--r-- | modules/remote/tests/remote_loopback.cpp | 82 |
3 files changed, 240 insertions, 11 deletions
diff --git a/modules/remote/c++/remote.hpp b/modules/remote/c++/remote.hpp index 83e6042..62d79c9 100644 --- a/modules/remote/c++/remote.hpp +++ b/modules/remote/c++/remote.hpp @@ -5,6 +5,7 @@ #include <forstio/codec/data.hpp> #include <forstio/codec/interface.hpp> +#include <forstio/codec/schema_hash.hpp> #include <variant> @@ -99,6 +100,61 @@ struct tmpl_group_to_data_client_std_tuple<tmpl_group<T...>, Encoding, Remote> { using type = std::tuple<data_client<T,Encoding,Remote>...>; }; } +/** + * Identifier of an interface function on the wire. + * + * Keyed on the member name instead of the member index, so reordering an + * interface doesn't silently rebind calls between versions. + */ +template<string_literal Lit> +struct rpc_function_id { + static constexpr uint32_t value = hash_literal<Lit>::apply(0u); +}; + +namespace impl { +/** + * Translates a runtime function id back into the compile time member name + * of an interface. + * + * Needed by every backend where the called function arrives as data instead + * of as a template argument. + */ +template<typename Iface> +struct iface_dispatch { + static_assert(always_false<Iface>, "Expecting an Interface Type"); +}; + +template<typename... T, string_literal... Names> +struct iface_dispatch<schema::Interface<schema::Member<T,Names>...>> { + template<typename Ret, uint64_t i, typename Func> + static Ret apply_ele(uint32_t func_id, Func& func){ + if constexpr ( i < sizeof...(Names) ){ + constexpr string_literal Lit = parameter_key_pack_type<i, Names...>::literal; + + if(func_id == rpc_function_id<Lit>::value){ + return func.template operator()<Lit>(); + } + + return apply_ele<Ret, i+1u>(func_id, func); + } else { + (void) func_id; + (void) func; + return make_error<err::not_found>("No interface member with this function id"); + } + } + + /** + * Calls func.operator()<Name>() for the member matching the id. + * The return type has to be provided, since there is no member to + * deduce it from if the id doesn't match anything. + */ + template<typename Ret, typename Func> + static Ret apply(uint32_t func_id, Func&& func){ + return apply_ele<Ret, 0u>(func_id, func); + } +}; +} + template<typename Remote> class i_rpc_server { protected: diff --git a/modules/remote/c++/remote_loopback.hpp b/modules/remote/c++/remote_loopback.hpp index d8cd7fa..3f39e41 100644 --- a/modules/remote/c++/remote_loopback.hpp +++ b/modules/remote/c++/remote_loopback.hpp @@ -111,16 +111,19 @@ public: * The parameter is either already present on the remote, in which case * only its id is handed over, or it is sent through the data layer * first. + * + * The id of the response is picked by the server and travels back with + * the answer. A client can't know which ids are free, so this is + * asynchronous in the id as well, not only in the data. */ template<string_literal Name> - error_or< + conveyor< remote_data<typename schema_member_type<Name, Iface>::type::ResponseT, Encoding, rmt::Loopback> > call(data_or_id<typename schema_member_type<Name, Iface>::type::RequestT, Encoding> input){ using Req = typename schema_member_type<Name, Iface>::type::RequestT; using Resp = typename schema_member_type<Name, Iface>::type::ResponseT; auto& req_client = get_data_client<Req>(); - auto& resp_client = get_data_client<Resp>(); /** * Resolve the parameter to an id on the remote. @@ -136,14 +139,37 @@ public: return std::move(eo_req_id.get_error()); } - /** - * Reserve the slot the response is supposed to end up in. - */ - auto eo_resp_id = resp_client.next_free_id(); + auto eo_resp_id = srv_().template call<Name>(eo_req_id.get_value()); if(eo_resp_id.is_error()){ return std::move(eo_resp_id.get_error()); } - auto resp_id = eo_resp_id.get_value(); + + return request_data<Resp>(eo_resp_id.get_value()); + } + + /** + * Call a function on the remote and store the response in a slot which + * was allocated beforehand. + */ + template<string_literal Name> + conveyor< + remote_data<typename schema_member_type<Name, Iface>::type::ResponseT, Encoding, rmt::Loopback> + > call(data_or_id<typename schema_member_type<Name, Iface>::type::RequestT, Encoding> input, id<typename schema_member_type<Name, Iface>::type::ResponseT> resp_id){ + using Req = typename schema_member_type<Name, Iface>::type::RequestT; + using Resp = typename schema_member_type<Name, Iface>::type::ResponseT; + + auto& req_client = get_data_client<Req>(); + + auto eo_req_id = [&]() -> error_or<id<Req>> { + if(input.is_id()){ + return input.get_id(); + } + + return req_client.send(input.get_data()); + }(); + if(eo_req_id.is_error()){ + return std::move(eo_req_id.get_error()); + } { auto eov = srv_().template call<Name>(eo_req_id.get_value(), resp_id); @@ -211,8 +237,69 @@ public: return dat_srvs_; } + /** + * Call a function which is only known at runtime. + * + * This is the entry point for every backend transporting the call + * itself. The ids are untyped here, since the wire only knows the + * function id and a number. The id the response ended up in is meant + * to travel back in the reply. + */ + error_or<uint64_t> call_by_id(uint32_t func_id, uint64_t id_param){ + auto dispatch = [&]<string_literal Lit>() -> error_or<uint64_t> { + using Req = typename schema_member_type<Lit, Iface>::type::RequestT; + + auto eov = this->template call<Lit>(id<Req>{id_param}); + if(eov.is_error()){ + return std::move(eov.get_error()); + } + + return eov.get_value().get_value(); + }; + + return impl::iface_dispatch<Iface>::template apply<error_or<uint64_t>>(func_id, dispatch); + } + + /** + * Runtime call into a slot the caller allocated beforehand. + */ + error_or<void> call_by_id(uint32_t func_id, uint64_t id_param, uint64_t id_resp){ + auto dispatch = [&]<string_literal Lit>() -> error_or<void> { + using Req = typename schema_member_type<Lit, Iface>::type::RequestT; + using Resp = typename schema_member_type<Lit, Iface>::type::ResponseT; + + return this->template call<Lit>(id<Req>{id_param}, id<Resp>{id_resp}); + }; + + return impl::iface_dispatch<Iface>::template apply<error_or<void>>(func_id, dispatch); + } + + /** + * Call a function and let the server decide where the response is + * stored. Clients can't know which ids are free without asking, so + * this is the variant which doesn't need a roundtrip beforehand. + */ + template<string_literal Lit> + error_or<id<typename schema_member_type<Lit,Iface>::type::ResponseT>> call(id<typename schema_member_type<Lit, Iface>::type::RequestT> id_param){ + return call_impl<Lit>(id_param, {}); + } + + /** + * Call a function and store the response in a slot which was allocated + * beforehand. + */ template<string_literal Lit> error_or<void> call(id<typename schema_member_type<Lit, Iface>::type::RequestT> id_param, id<typename schema_member_type<Lit,Iface>::type::ResponseT> id_resp){ + auto eov = call_impl<Lit>(id_param, id_resp); + if(eov.is_error()){ + return std::move(eov.get_error()); + } + + return make_void(); + } +private: + template<string_literal Lit> + error_or<id<typename schema_member_type<Lit,Iface>::type::ResponseT>> call_impl(id<typename schema_member_type<Lit, Iface>::type::RequestT> id_param, maybe<id<typename schema_member_type<Lit,Iface>::type::ResponseT>> id_resp){ using Req = typename schema_member_type<Lit,Iface>::type::RequestT; auto req_dat_ptr = std::get<ptr<data_server<Req,Encode,rmt::Loopback>>>(dat_srvs_); auto eo_req_param = req_dat_ptr().find(id_param); @@ -230,14 +317,20 @@ public: using Resp = typename schema_member_type<Lit,Iface>::type::ResponseT; auto resp_dat_ptr = std::get<ptr<data_server<Resp,Encode,rmt::Loopback>>>(dat_srvs_); - auto& resp_test_ptr = resp_dat_ptr(); + auto& resp_dat_srv = resp_dat_ptr(); - auto eo_send = resp_test_ptr.send(std::move(answer), id_resp); + /** + * The answer exists at this point, so this is the latest and + * therefore most accurate moment to pick a free slot. + */ + id<Resp> store_id = id_resp.has_value() ? id_resp.value() : resp_dat_srv.next_free_id(); + + auto eo_send = resp_dat_srv.send(std::move(answer), store_id); if(eo_send.is_error()){ return std::move(eo_send.get_error()); } - return make_void(); + return store_id; } }; diff --git a/modules/remote/tests/remote_loopback.cpp b/modules/remote/tests/remote_loopback.cpp index dbdd0f1..4460ad1 100644 --- a/modules/remote/tests/remote_loopback.cpp +++ b/modules/remote/tests/remote_loopback.cpp @@ -65,6 +65,85 @@ SAW_TEST("Remote Loopback RPC"){ } } +SAW_TEST("Remote Loopback RPC Runtime Dispatch"){ + using namespace saw; + + event_loop loop; + wait_scope wait{loop}; + + remote<rmt::Loopback> rmt; + + auto eov = rmt.parse_address(0u); + SAW_EXPECT(eov.is_value(), "Didn't parse correctly"); + auto& addr = eov.get_value(); + + auto eo_u32_srv = rmt.template data_listen<sch::UInt32, encode::Native>(*addr); + SAW_EXPECT(eo_u32_srv.is_value(), std::string{"Couldn't listen: "} + std::string{eo_u32_srv.get_error().get_category()}); + auto& u32_srv = eo_u32_srv.get_value(); + + auto eo_i64_srv = rmt.template data_listen<sch::Int64, encode::Native>(*addr); + SAW_EXPECT(eo_i64_srv.is_value(), std::string{"Couldn't listen: "} + std::string{eo_i64_srv.get_error().get_category()}); + auto& i64_srv = eo_i64_srv.get_value(); + + auto foo_func = [](data<sch::UInt32, encode::Native> input) -> error_or<data<sch::Int64, encode::Native>> { + data<sch::Int64, encode::Native> rv; + rv.set(input.get() * 2); + return rv; + }; + + auto iface = interface_factory<sch::TestInterface, encode::Native>::create(std::move(foo_func)); + + auto eo_rpc_srv = rmt.template rpc_listen<sch::TestInterface, encode::Native>(*addr, std::move(iface)); + SAW_EXPECT(eo_rpc_srv.is_value(), std::string{"Couldn't listen: "} + std::string{eo_rpc_srv.get_error().get_category()}); + auto& rpc_srv = eo_rpc_srv.get_value(); + + id<sch::UInt32> id32{0u}; + /** + * Deliberately not 0, since the server picks that one for the first + * response below. + */ + id<sch::Int64> id64{7u}; + + data<sch::UInt32, encode::Native> inp{21}; + + { + auto eo_send = u32_srv->send(inp, id32); + SAW_EXPECT(eo_send.is_value(), "Failed send."); + } + + /** + * This is the path a backend takes which transports the call itself. + * Only the function id and the raw parameter id are known. The id the + * server picked for the response is meant to travel back in the reply. + */ + { + auto eo_called = rpc_srv->call_by_id(rpc_function_id<"foo">::value, id32.get_value()); + SAW_EXPECT(eo_called.is_value(), (std::string{"Failed call: "} + std::string{eo_called.get_error().get_category()})); + + id<sch::Int64> resp_id{eo_called.get_value()}; + auto eo_recv = i64_srv->receive(resp_id); + SAW_EXPECT(eo_recv.is_value(), "Failed receive."); + auto& recv = eo_recv.get_value(); + SAW_EXPECT((recv.get() == 42), (std::string{"Wrong value received: "} + std::to_string(recv.get()))); + } + /** + * Calling into a slot the caller picked beforehand. + */ + { + auto eo_called = rpc_srv->call_by_id(rpc_function_id<"foo">::value, id32.get_value(), id64.get_value()); + SAW_EXPECT(eo_called.is_value(), (std::string{"Failed call: "} + std::string{eo_called.get_error().get_category()})); + + auto eo_recv = i64_srv->receive(id64); + SAW_EXPECT(eo_recv.is_value(), "Failed receive."); + auto& recv = eo_recv.get_value(); + SAW_EXPECT((recv.get() == 42), (std::string{"Wrong value received: "} + std::to_string(recv.get()))); + } + { + auto eo_called = rpc_srv->call_by_id(rpc_function_id<"foo">::value + 1u, id32.get_value()); + SAW_EXPECT(eo_called.is_error(), "An unknown function id should not be callable."); + } +} + SAW_TEST("Remote Loopback RPC Client"){ using namespace saw; @@ -101,7 +180,8 @@ SAW_TEST("Remote Loopback RPC Client"){ data<sch::UInt32, encode::Native> inp{21}; - auto eo_rmt_dat = client.template call<"foo">(std::move(inp)); + auto cvr_rmt_dat = client.template call<"foo">(std::move(inp)); + auto eo_rmt_dat = cvr_rmt_dat.take(); SAW_EXPECT(eo_rmt_dat.is_value(), (std::string{"Failed call: "} + std::string{eo_rmt_dat.get_error().get_category()})); auto& rmt_dat = eo_rmt_dat.get_value(); |
