From 55fb3a9221ea91453573453288ecedb6ea79b7ba Mon Sep 17 00:00:00 2001 From: "Claudius \"keldu\" Holeksa" Date: Sat, 7 Nov 2026 17:30:41 +0100 Subject: Creating out of source doc files --- modules/remote-io/.nix/derivation.nix | 2 +- modules/remote/c++/remote.hpp | 56 +++++++++++++++ modules/remote/c++/remote_loopback.hpp | 113 ++++++++++++++++++++++++++++--- modules/remote/tests/remote_loopback.cpp | 82 +++++++++++++++++++++- 4 files changed, 241 insertions(+), 12 deletions(-) (limited to 'modules') diff --git a/modules/remote-io/.nix/derivation.nix b/modules/remote-io/.nix/derivation.nix index 917789a..ce17e95 100644 --- a/modules/remote-io/.nix/derivation.nix +++ b/modules/remote-io/.nix/derivation.nix @@ -8,7 +8,7 @@ }: stdenv.mkDerivation { - pname = "forstio-remote-thread"; + pname = "forstio-remote-io"; inherit version; src = ./..; 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 #include +#include #include @@ -99,6 +100,61 @@ struct tmpl_group_to_data_client_std_tuple, Encoding, Remote> { using type = std::tuple...>; }; } +/** + * 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 +struct rpc_function_id { + static constexpr uint32_t value = hash_literal::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 +struct iface_dispatch { + static_assert(always_false, "Expecting an Interface Type"); +}; + +template +struct iface_dispatch...>> { + template + static Ret apply_ele(uint32_t func_id, Func& func){ + if constexpr ( i < sizeof...(Names) ){ + constexpr string_literal Lit = parameter_key_pack_type::literal; + + if(func_id == rpc_function_id::value){ + return func.template operator()(); + } + + return apply_ele(func_id, func); + } else { + (void) func_id; + (void) func; + return make_error("No interface member with this function id"); + } + } + + /** + * Calls func.operator()() 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 + static Ret apply(uint32_t func_id, Func&& func){ + return apply_ele(func_id, func); + } +}; +} + template 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 - error_or< + conveyor< remote_data::type::ResponseT, Encoding, rmt::Loopback> > call(data_or_id::type::RequestT, Encoding> input){ using Req = typename schema_member_type::type::RequestT; using Resp = typename schema_member_type::type::ResponseT; auto& req_client = get_data_client(); - auto& resp_client = get_data_client(); /** * 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(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(eo_resp_id.get_value()); + } + + /** + * Call a function on the remote and store the response in a slot which + * was allocated beforehand. + */ + template + conveyor< + remote_data::type::ResponseT, Encoding, rmt::Loopback> + > call(data_or_id::type::RequestT, Encoding> input, id::type::ResponseT> resp_id){ + using Req = typename schema_member_type::type::RequestT; + using Resp = typename schema_member_type::type::ResponseT; + + auto& req_client = get_data_client(); + + auto eo_req_id = [&]() -> error_or> { + 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(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 call_by_id(uint32_t func_id, uint64_t id_param){ + auto dispatch = [&]() -> error_or { + using Req = typename schema_member_type::type::RequestT; + + auto eov = this->template call(id{id_param}); + if(eov.is_error()){ + return std::move(eov.get_error()); + } + + return eov.get_value().get_value(); + }; + + return impl::iface_dispatch::template apply>(func_id, dispatch); + } + + /** + * Runtime call into a slot the caller allocated beforehand. + */ + error_or call_by_id(uint32_t func_id, uint64_t id_param, uint64_t id_resp){ + auto dispatch = [&]() -> error_or { + using Req = typename schema_member_type::type::RequestT; + using Resp = typename schema_member_type::type::ResponseT; + + return this->template call(id{id_param}, id{id_resp}); + }; + + return impl::iface_dispatch::template apply>(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 + error_or::type::ResponseT>> call(id::type::RequestT> id_param){ + return call_impl(id_param, {}); + } + + /** + * Call a function and store the response in a slot which was allocated + * beforehand. + */ template error_or call(id::type::RequestT> id_param, id::type::ResponseT> id_resp){ + auto eov = call_impl(id_param, id_resp); + if(eov.is_error()){ + return std::move(eov.get_error()); + } + + return make_void(); + } +private: + template + error_or::type::ResponseT>> call_impl(id::type::RequestT> id_param, maybe::type::ResponseT>> id_resp){ using Req = typename schema_member_type::type::RequestT; auto req_dat_ptr = std::get>>(dat_srvs_); auto eo_req_param = req_dat_ptr().find(id_param); @@ -230,14 +317,20 @@ public: using Resp = typename schema_member_type::type::ResponseT; auto resp_dat_ptr = std::get>>(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 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; + + 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(*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(*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 input) -> error_or> { + data rv; + rv.set(input.get() * 2); + return rv; + }; + + auto iface = interface_factory::create(std::move(foo_func)); + + auto eo_rpc_srv = rmt.template rpc_listen(*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 id32{0u}; + /** + * Deliberately not 0, since the server picks that one for the first + * response below. + */ + id id64{7u}; + + data 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 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 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(); -- cgit v1.3.1