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>