Add sdbusplus::asio coroutine method handling
Adding the server-side of the coroutine path allows yielding
asynchronous method handling via coroutines. This means that a method
handler can call a yielding dbus call (or other asio-based asynchronous
call) without blocking the rest of the process.
The call path might look something like this:
service.thing/object/path/interface.my-method()
- do something
- yield_method_call(other.service, /other/path,
other.interface, other-method)
<yields to other coroutine>
execute other code in another context
<returns some time later with dbus call's response>
- use response from other method
<- return my-method response
This also changes the asio-example, pulling it apart into a
client/server model so it is more clear about how to use the yielding
async method handling and yielding async method calls.
Change-Id: I23ccf7a9a8dff787be78929959c1f018280a0392
Signed-off-by: Vernon Mauery <vernon.mauery@linux.intel.com>
diff --git a/example/asio-example.cpp b/example/asio-example.cpp
index 9819eba..b4f6265 100644
--- a/example/asio-example.cpp
+++ b/example/asio-example.cpp
@@ -7,22 +7,47 @@
#include <sdbusplus/asio/object_server.hpp>
#include <sdbusplus/asio/sd_event.hpp>
#include <sdbusplus/bus.hpp>
+#include <sdbusplus/exception.hpp>
#include <sdbusplus/server.hpp>
#include <sdbusplus/timer.hpp>
using variant = sdbusplus::message::variant<int, std::string>;
+
int foo(int test)
{
+ std::cout << "foo(" << test << ") -> " << (test + 1) << "\n";
return ++test;
}
+// called from coroutine context, can make yielding dbus calls
+int fooYield(boost::asio::yield_context yield,
+ std::shared_ptr<sdbusplus::asio::connection> conn, int test)
+{
+ // fetch the real value from testFunction
+ boost::system::error_code ec;
+ std::cout << "fooYield(yield, " << test << ")...\n";
+ int testCount = conn->yield_method_call<int>(
+ yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+ "xyz.openbmc_project.test", "TestFunction", test);
+ if (ec || testCount != (test + 1))
+ {
+ std::cout << "call to foo failed: ec = " << ec << '\n';
+ return -1;
+ }
+ std::cout << "yielding call to foo OK! (-> " << testCount << ")\n";
+ return testCount;
+}
+
int methodWithMessage(sdbusplus::message::message& m, int test)
{
+ std::cout << "methodWithMessage(m, " << test << ") -> " << (test + 1)
+ << "\n";
return ++test;
}
int voidBar(void)
{
+ std::cout << "voidBar() -> 42\n";
return 42;
}
@@ -66,21 +91,75 @@
}
}
-void do_start_async_method_call_two(
- std::shared_ptr<sdbusplus::asio::connection> conn,
- boost::asio::yield_context yield)
+void do_start_async_ipmi_call(std::shared_ptr<sdbusplus::asio::connection> conn,
+ boost::asio::yield_context yield)
+{
+ auto method = conn->new_method_call("xyz.openbmc_project.asio-test",
+ "/xyz/openbmc_project/test",
+ "xyz.openbmc_project.test", "execute");
+ constexpr uint8_t netFn = 6;
+ constexpr uint8_t lun = 0;
+ constexpr uint8_t cmd = 1;
+ std::map<std::string, variant> options = {{"username", variant("admin")},
+ {"privilege", variant(4)}};
+ std::vector<uint8_t> commandData = {4, 3, 2, 1};
+ method.append(netFn, lun, cmd, commandData, options);
+ boost::system::error_code ec;
+ sdbusplus::message::message reply = conn->async_send(method, yield[ec]);
+ std::tuple<uint8_t, uint8_t, uint8_t, uint8_t, std::vector<uint8_t>>
+ tupleOut;
+ try
+ {
+ reply.read(tupleOut);
+ }
+ catch (const sdbusplus::exception::SdBusError& e)
+ {
+ std::cerr << "failed to unpack; sig is " << reply.get_signature()
+ << "\n";
+ }
+ auto& [rnetFn, rlun, rcmd, cc, responseData] = tupleOut;
+ std::vector<uint8_t> expRsp = {1, 2, 3, 4};
+ if (rnetFn == uint8_t(netFn + 1) && rlun == lun && rcmd == cmd && cc == 0 &&
+ responseData == expRsp)
+ {
+ std::cerr << "ipmi call returns OK!\n";
+ }
+ else
+ {
+ std::cerr << "ipmi call returns unexpected response\n";
+ }
+}
+
+auto ipmiInterface(boost::asio::yield_context yield, uint8_t netFn, uint8_t lun,
+ uint8_t cmd, std::vector<uint8_t>& data,
+ const std::map<std::string, variant>& options)
+{
+ std::vector<uint8_t> reply = {1, 2, 3, 4};
+ uint8_t cc = 0;
+ std::cerr << "ipmiInterface:execute(" << int(netFn) << int(cmd) << ")\n";
+ return std::make_tuple(uint8_t(netFn + 1), lun, cmd, cc, reply);
+}
+
+void do_start_async_to_yield(std::shared_ptr<sdbusplus::asio::connection> conn,
+ boost::asio::yield_context yield)
{
boost::system::error_code ec;
- int32_t testCount;
- std::string testValue;
- std::tie(testCount, testValue) =
- conn->yield_method_call<int32_t, std::string>(
+ int testValue = 0;
+ try
+ {
+ testValue = conn->yield_method_call<int>(
yield[ec], "xyz.openbmc_project.asio-test",
"/xyz/openbmc_project/test", "xyz.openbmc_project.test",
- "TestMethod", int32_t(42));
- if (!ec && testCount == 42 && testValue == "success: 42")
+ "TestYieldFunction", int(41));
+ }
+ catch (sdbusplus::exception::SdBusError& e)
{
- std::cout << "async call to TestMethod serialized via yield OK!\n";
+ std::cout << "oops: " << e.what() << "\n";
+ }
+ if (!ec && testValue == 42)
+ {
+ std::cout
+ << "yielding call to TestYieldFunction serialized via yield OK!\n";
}
else
{
@@ -88,60 +167,12 @@
}
}
-int main()
+int server()
{
- using GetSubTreeType = std::vector<std::pair<
- std::string,
- std::vector<std::pair<std::string, std::vector<std::string>>>>>;
- using message = sdbusplus::message::message;
// setup connection to dbus
boost::asio::io_service io;
auto conn = std::make_shared<sdbusplus::asio::connection>(io);
- // test async method call and async send
- auto mesg =
- conn->new_method_call("xyz.openbmc_project.ObjectMapper",
- "/xyz/openbmc_project/object_mapper",
- "xyz.openbmc_project.ObjectMapper", "GetSubTree");
-
- static const auto depth = 2;
- static const std::vector<std::string> interfaces = {
- "xyz.openbmc_project.Sensor.Value"};
- mesg.append("/xyz/openbmc_project/Sensors", depth, interfaces);
-
- conn->async_send(mesg, [](boost::system::error_code ec, message& ret) {
- std::cout << "async_send callback\n";
- if (ec || ret.is_method_error())
- {
- std::cerr << "error with async_send\n";
- return;
- }
- GetSubTreeType data;
- ret.read(data);
- for (auto& item : data)
- {
- std::cout << item.first << "\n";
- }
- });
-
- conn->async_method_call(
- [](boost::system::error_code ec, GetSubTreeType& subtree) {
- std::cout << "async_method_call callback\n";
- if (ec)
- {
- std::cerr << "error with async_method_call\n";
- return;
- }
- for (auto& item : subtree)
- {
- std::cout << item.first << "\n";
- }
- },
- "xyz.openbmc_project.ObjectMapper",
- "/xyz/openbmc_project/object_mapper",
- "xyz.openbmc_project.ObjectMapper", "GetSubTree",
- "/org/openbmc/control", 2, std::vector<std::string>());
-
// test object server
conn->request_name("xyz.openbmc_project.asio-test");
auto server = sdbusplus::asio::object_server(conn);
@@ -191,12 +222,99 @@
iface->register_method("TestFunction", foo);
+ // fooYield has boost::asio::yield_context as first argument
+ // so will be executed in coroutine context if called
+ iface->register_method("TestYieldFunction",
+ [conn](boost::asio::yield_context yield, int val) {
+ return fooYield(yield, conn, val);
+ });
+
iface->register_method("TestMethodWithMessage", methodWithMessage);
iface->register_method("VoidFunctionReturnsInt", voidBar);
+ iface->register_method("execute", ipmiInterface);
+
iface->initialize();
- iface->set_property("int", 45);
+
+ io.run();
+
+ return 0;
+}
+
+int client()
+{
+ using GetSubTreeType = std::vector<std::pair<
+ std::string,
+ std::vector<std::pair<std::string, std::vector<std::string>>>>>;
+ using message = sdbusplus::message::message;
+
+ // setup connection to dbus
+ boost::asio::io_service io;
+ auto conn = std::make_shared<sdbusplus::asio::connection>(io);
+
+ int ready = 0;
+ while (!ready)
+ {
+ auto readyMsg = conn->new_method_call(
+ "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+ "xyz.openbmc_project.test", "VoidFunctionReturnsInt");
+ try
+ {
+ message intMsg = conn->call(readyMsg);
+ intMsg.read(ready);
+ }
+ catch (sdbusplus::exception::SdBusError& e)
+ {
+ ready = 0;
+ // pause to give the server a chance to start up
+ usleep(10000);
+ }
+ }
+
+ // test async method call and async send
+ auto mesg =
+ conn->new_method_call("xyz.openbmc_project.ObjectMapper",
+ "/xyz/openbmc_project/object_mapper",
+ "xyz.openbmc_project.ObjectMapper", "GetSubTree");
+
+ static const auto depth = 2;
+ static const std::vector<std::string> interfaces = {
+ "xyz.openbmc_project.Sensor.Value"};
+ mesg.append("/xyz/openbmc_project/Sensors", depth, interfaces);
+
+ conn->async_send(mesg, [](boost::system::error_code ec, message& ret) {
+ std::cout << "async_send callback\n";
+ if (ec || ret.is_method_error())
+ {
+ std::cerr << "error with async_send\n";
+ return;
+ }
+ GetSubTreeType data;
+ ret.read(data);
+ for (auto& item : data)
+ {
+ std::cout << item.first << "\n";
+ }
+ });
+
+ conn->async_method_call(
+ [](boost::system::error_code ec, GetSubTreeType& subtree) {
+ std::cout << "async_method_call callback\n";
+ if (ec)
+ {
+ std::cerr << "error with async_method_call\n";
+ return;
+ }
+ for (auto& item : subtree)
+ {
+ std::cout << item.first << "\n";
+ }
+ },
+ "xyz.openbmc_project.ObjectMapper",
+ "/xyz/openbmc_project/object_mapper",
+ "xyz.openbmc_project.ObjectMapper", "GetSubTree",
+ "/org/openbmc/control", 2, std::vector<std::string>());
// sd_events work too using the default event loop
phosphor::Timer t1([]() { std::cerr << "*** tock ***\n"; });
@@ -212,9 +330,53 @@
do_start_async_method_call_one(conn, yield);
});
boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
- do_start_async_method_call_two(conn, yield);
+ do_start_async_ipmi_call(conn, yield);
});
+ boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
+ do_start_async_to_yield(conn, yield);
+ });
+
+ conn->async_method_call(
+ [](boost::system::error_code ec, int32_t testValue) {
+ if (ec)
+ {
+ std::cerr << "TestYieldFunction returned error with "
+ "async_method_call (ec = "
+ << ec << ")\n";
+ return;
+ }
+ std::cout << "TestYieldFunction return " << testValue << "\n";
+ },
+ "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
+ "xyz.openbmc_project.test", "TestYieldFunction", int32_t(41));
io.run();
return 0;
}
+
+int main(int argc, const char* argv[])
+{
+ if (argc == 1)
+ {
+ int pid = fork();
+ if (pid == 0)
+ {
+ return client();
+ }
+ else if (pid > 0)
+ {
+ return server();
+ }
+ return pid;
+ }
+ if (std::string(argv[1]) == "--server")
+ {
+ return server();
+ }
+ if (std::string(argv[1]) == "--client")
+ {
+ return client();
+ }
+ std::cout << "usage: " << argv[0] << " [--server | --client]\n";
+ return -1;
+}
diff --git a/sdbusplus/asio/connection.hpp b/sdbusplus/asio/connection.hpp
index 0c7f80e..ec54ae4 100644
--- a/sdbusplus/asio/connection.hpp
+++ b/sdbusplus/asio/connection.hpp
@@ -15,6 +15,12 @@
*/
#pragma once
+#ifndef BOOST_COROUTINES_NO_DEPRECATION_WARNING
+// users should define this if they directly include boost/asio/spawn.hpp,
+// but by defining it here, warnings won't cause problems with a compile
+#define BOOST_COROUTINES_NO_DEPRECATION_WARNING
+#endif
+
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/callable_traits.hpp>
@@ -196,6 +202,11 @@
}
}
+ boost::asio::io_service& get_io_service()
+ {
+ return io_;
+ }
+
private:
boost::asio::io_service& io_;
boost::asio::posix::stream_descriptor socket;
diff --git a/sdbusplus/asio/object_server.hpp b/sdbusplus/asio/object_server.hpp
index b0698ba..eee6e2f 100644
--- a/sdbusplus/asio/object_server.hpp
+++ b/sdbusplus/asio/object_server.hpp
@@ -1,14 +1,23 @@
#pragma once
+#ifndef BOOST_COROUTINES_NO_DEPRECATION_WARNING
+// users should define this if they directly include boost/asio/spawn.hpp,
+// but by defining it here, warnings won't cause problems with a compile
+#define BOOST_COROUTINES_NO_DEPRECATION_WARNING
+#endif
+
#include <boost/any.hpp>
+#include <boost/asio/spawn.hpp>
#include <boost/container/flat_map.hpp>
#include <list>
+#include <optional>
#include <sdbusplus/asio/connection.hpp>
#include <sdbusplus/exception.hpp>
#include <sdbusplus/message/read.hpp>
#include <sdbusplus/message/types.hpp>
#include <sdbusplus/server.hpp>
#include <sdbusplus/utility/tuple_to_array.hpp>
+#include <sdbusplus/utility/type_traits.hpp>
namespace sdbusplus
{
@@ -30,11 +39,65 @@
};
template <typename T>
+using FirstArgIsYield =
+ std::is_same<typename utility::get_first_arg<typename utility::decay_tuple<
+ boost::callable_traits::args_t<T>>::type>::type,
+ boost::asio::yield_context>;
+
+template <typename T>
using FirstArgIsMessage =
std::is_same<typename utility::get_first_arg<typename utility::decay_tuple<
boost::callable_traits::args_t<T>>::type>::type,
message::message>;
+template <typename T>
+using SecondArgIsMessage = std::is_same<
+ typename utility::get_first_arg<
+ typename utility::strip_first_arg<typename utility::decay_tuple<
+ boost::callable_traits::args_t<T>>::type>::type>::type,
+ message::message>;
+template <typename T>
+static constexpr bool callbackYields = FirstArgIsYield<T>::value;
+template <typename T>
+static constexpr bool callbackWantsMessage = (FirstArgIsMessage<T>::value ||
+ SecondArgIsMessage<T>::value);
+
+#ifdef __cpp_if_constexpr
+namespace details
+{
+// small helper class to count the number of non-dbus arguments
+// to a registered dbus function (like message::message or yield_context)
+// so the registered signature can omit them
+template <typename FirstArg, typename... Rest>
+struct NonDbusArgsCount;
+
+template <>
+struct NonDbusArgsCount<std::tuple<>>
+{
+ constexpr static std::size_t size()
+ {
+ return 0;
+ }
+};
+template <typename FirstArg, typename... OtherArgs>
+struct NonDbusArgsCount<std::tuple<FirstArg, OtherArgs...>>
+{
+ constexpr static std::size_t size()
+ {
+ if constexpr (std::is_same<FirstArg, message::message>::value ||
+ std::is_same<FirstArg, boost::asio::yield_context>::value)
+ {
+ return 1 + NonDbusArgsCount<std::tuple<OtherArgs...>>::size();
+ }
+ else
+ {
+ return NonDbusArgsCount<std::tuple<OtherArgs...>>::size();
+ }
+ }
+};
+} // namespace details
+#endif // __cpp_if_constexpr
+
template <typename CallbackType>
class callback_method_instance : public callback
{
@@ -44,7 +107,7 @@
}
int call(message::message& m) override
{
- return expandCall<CallbackType>(m);
+ return expandCall(m);
}
private:
@@ -66,30 +129,37 @@
{
std::experimental::apply(func_, inputArgs);
}
+#ifdef __cpp_if_constexpr
// optional message-first-argument callback
- template <typename T>
- std::enable_if_t<FirstArgIsMessage<T>::value, int>
- expandCall(message::message& m)
+ int expandCall(message::message& m)
{
- using DbusTupleType =
- typename utility::strip_first_arg<InputTupleType>::type;
+ using DbusTupleType = typename utility::strip_first_n_args<
+ details::NonDbusArgsCount<InputTupleType>::size(),
+ InputTupleType>::type;
+
DbusTupleType dbusArgs;
if (!utility::read_into_tuple(dbusArgs, m))
{
return -EINVAL;
}
-
auto ret = m.new_method_return();
- InputTupleType inputArgs =
- std::tuple_cat(std::forward_as_tuple(std::move(m)), dbusArgs);
- callFunction<ResultType>(ret, inputArgs);
+ std::optional<InputTupleType> inputArgs;
+ if constexpr (callbackWantsMessage<CallbackType>)
+ {
+ inputArgs.emplace(
+ std::tuple_cat(std::forward_as_tuple(std::move(m)), dbusArgs));
+ }
+ else
+ {
+ inputArgs.emplace(dbusArgs);
+ }
+ callFunction<ResultType>(ret, *inputArgs);
ret.method_return();
return 1;
};
+#else
// normal dbus-types-only callback
- template <typename T>
- std::enable_if_t<!FirstArgIsMessage<T>::value, int>
- expandCall(message::message& m)
+ int expandCall(message::message& m)
{
InputTupleType inputArgs;
if (!utility::read_into_tuple(inputArgs, m))
@@ -102,8 +172,90 @@
ret.method_return();
return 1;
};
+#endif
};
+#ifdef __cpp_if_constexpr
+template <typename CallbackType>
+class coroutine_method_instance : public callback
+{
+ public:
+ coroutine_method_instance(boost::asio::io_service& io,
+ CallbackType&& func) :
+ io_(io),
+ func_(std::move(func))
+ {
+ }
+ int call(message::message& m) override
+ {
+ // make a copy of m to move into the coroutine
+ message::message b{m};
+ // spawn off a new coroutine to handle the method call
+ boost::asio::spawn(
+ io_, [this, b = std::move(b)](boost::asio::yield_context yield) {
+ message::message mcpy{std::move(b)};
+ expandCall(yield, mcpy);
+ });
+ return 1;
+ }
+
+ private:
+ using CallbackSignature = boost::callable_traits::args_t<CallbackType>;
+ using InputTupleType =
+ typename utility::decay_tuple<CallbackSignature>::type;
+ using ResultType = boost::callable_traits::return_type_t<CallbackType>;
+ boost::asio::io_service& io_;
+ CallbackType func_;
+ template <typename T>
+ std::enable_if_t<!std::is_void<T>::value, void>
+ callFunction(message::message& m, InputTupleType& inputArgs)
+ {
+ ResultType r = std::experimental::apply(func_, inputArgs);
+ m.append(r);
+ }
+ template <typename T>
+ std::enable_if_t<std::is_void<T>::value, void>
+ callFunction(message::message& m, InputTupleType& inputArgs)
+ {
+ std::experimental::apply(func_, inputArgs);
+ }
+ // co-routine body for call
+ void expandCall(boost::asio::yield_context yield, message::message& m)
+ {
+ using DbusTupleType = typename utility::strip_first_n_args<
+ details::NonDbusArgsCount<InputTupleType>::size(),
+ InputTupleType>::type;
+ DbusTupleType dbusArgs;
+ try
+ {
+ utility::read_into_tuple(dbusArgs, m);
+ }
+ catch (const exception::SdBusError& e)
+ {
+ auto ret = m.new_method_errno(-EINVAL);
+ ret.method_return();
+ return;
+ }
+
+ auto ret = m.new_method_return();
+ std::optional<InputTupleType> inputArgs;
+ if constexpr (callbackWantsMessage<CallbackType>)
+ {
+ inputArgs.emplace(
+ std::tuple_cat(std::forward_as_tuple(std::move(yield)),
+ std::forward_as_tuple(std::move(m)), dbusArgs));
+ }
+ else
+ {
+ inputArgs.emplace(std::tuple_cat(
+ std::forward_as_tuple(std::move(yield)), dbusArgs));
+ }
+ callFunction<ResultType>(ret, *inputArgs);
+ ret.method_return();
+ };
+};
+#endif // __cpp_if_constexpr
+
template <typename PropertyType, typename CallbackType>
class callback_get_instance : public callback
{
@@ -321,12 +473,14 @@
return false;
}
+#ifdef __cpp_if_constexpr
template <typename CallbackType>
- std::enable_if_t<FirstArgIsMessage<CallbackType>::value, bool>
- register_method(const std::string& name, CallbackType&& handler)
+ bool register_method(const std::string& name, CallbackType&& handler)
{
- using CallbackSignature = typename utility::strip_first_arg<
- boost::callable_traits::args_t<CallbackType>>::type;
+ using ActualSignature = boost::callable_traits::args_t<CallbackType>;
+ using CallbackSignature = typename utility::strip_first_n_args<
+ details::NonDbusArgsCount<ActualSignature>::size(),
+ ActualSignature>::type;
using InputTupleType =
typename utility::decay_tuple<CallbackSignature>::type;
using ResultType = boost::callable_traits::return_type_t<CallbackType>;
@@ -342,18 +496,28 @@
auto nameItr = methodNames_.emplace(methodNames_.end(), name);
- callbacksMethod_[name] =
- std::make_unique<callback_method_instance<CallbackType>>(
- std::move(handler));
+ if constexpr (callbackYields<CallbackType>)
+ {
+ callbacksMethod_[name] =
+ std::make_unique<coroutine_method_instance<CallbackType>>(
+ conn_->get_io_service(), std::move(handler));
+ }
+ else
+ {
+ callbacksMethod_[name] =
+ std::make_unique<callback_method_instance<CallbackType>>(
+ std::move(handler));
+ }
vtable_.emplace_back(vtable::method(nameItr->c_str(), argType.data(),
resultType.data(), method_handler));
return true;
}
-
+#else // __cpp_if_constexpr not available
+ // without __cpp_if_constexpr, no support for message or yield in
+ // callback
template <typename CallbackType>
- std::enable_if_t<!FirstArgIsMessage<CallbackType>::value, bool>
- register_method(const std::string& name, CallbackType&& handler)
+ bool register_method(const std::string& name, CallbackType&& handler)
{
using CallbackSignature = boost::callable_traits::args_t<CallbackType>;
using InputTupleType =
@@ -379,6 +543,7 @@
resultType.data(), method_handler));
return true;
}
+#endif // __cpp_if_constexpr
static int get_handler(sd_bus* bus, const char* path, const char* interface,
const char* property, sd_bus_message* reply,
diff --git a/sdbusplus/utility/type_traits.hpp b/sdbusplus/utility/type_traits.hpp
index 28fa31e..77d266b 100644
--- a/sdbusplus/utility/type_traits.hpp
+++ b/sdbusplus/utility/type_traits.hpp
@@ -28,18 +28,30 @@
std::add_pointer_t<std::remove_extent_t<T>>, T>,
T>;
-// Small helper class for stripping off the error code from the function
-// argument definitions so unpack can be called appropriately
-template <typename T>
-struct strip_first_arg
+template <std::size_t N, typename FirstArg, typename... Rest>
+struct strip_first_n_args;
+
+template <std::size_t N, typename FirstArg, typename... Rest>
+struct strip_first_n_args<N, std::tuple<FirstArg, Rest...>>
+ : strip_first_n_args<N - 1, std::tuple<Rest...>>
{
};
template <typename FirstArg, typename... Rest>
-struct strip_first_arg<std::tuple<FirstArg, Rest...>>
+struct strip_first_n_args<0, std::tuple<FirstArg, Rest...>>
{
- using type = std::tuple<Rest...>;
+ using type = std::tuple<FirstArg, Rest...>;
};
+template <std::size_t N>
+struct strip_first_n_args<N, std::tuple<>>
+{
+ using type = std::tuple<>;
+};
+
+// Small helper class for stripping off the error code from the function
+// argument definitions so unpack can be called appropriately
+template <typename T>
+using strip_first_arg = strip_first_n_args<1, T>;
// matching helper class to only return the first type
template <typename T>