summaryrefslogtreecommitdiff
path: root/modules/remote
diff options
context:
space:
mode:
Diffstat (limited to 'modules/remote')
-rw-r--r--modules/remote/c++/remote.hpp56
-rw-r--r--modules/remote/c++/remote_loopback.hpp113
-rw-r--r--modules/remote/tests/remote_loopback.cpp82
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();