Skip to content

Commit

Permalink
Add coroutine support for sdbusplus::asio method calls
Browse files Browse the repository at this point in the history
Using a coroutine to asynchronously execute method calls gives the best
of both worlds:
1) better readability because the code reads like synchronous code
2) better throughput because it is actually asynchronous

When passed in a boost::asio::yield_context, the sdbusplus::asio dbus
connection members async_send and async_method_call will execute
asynchronously using coroutines.

This also adds an example of how this works in the
example/asio-example.cpp file.

Change-Id: Ifb71b2c757ecbfd16b3be95bdefc45a701ca0d51
Signed-off-by: Vernon Mauery <vernon.mauery@linux.intel.com>
  • Loading branch information
vmauery authored and wak-google committed Oct 12, 2018
1 parent fba332b commit 261e72b
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 5 deletions.
2 changes: 2 additions & 0 deletions example/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ asio_example_CXXFLAGS = \
-DBOOST_ALL_NO_LIB \
-DBOOST_SYSTEM_NO_DEPRECATED \
-DBOOST_ERROR_CODE_HEADER_ONLY \
-DBOOST_COROUTINES_NO_DEPRECATION_WARNING \
-I$(top_srcdir)

asio_example_LDADD = \
$(SYSTEMD_LIBS) \
$(PTHREAD_LIBS) \
-lboost_coroutine \
../libsdbusplus.la

asio_example_LDFLAGS = \
Expand Down
75 changes: 74 additions & 1 deletion example/asio-example.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <chrono>
#include <ctime>
#include <iostream>
Expand All @@ -9,6 +10,7 @@
#include <sdbusplus/server.hpp>
#include <sdbusplus/timer.hpp>

using variant = sdbusplus::message::variant<int, std::string>;
int foo(int test)
{
return ++test;
Expand All @@ -24,6 +26,68 @@ int voidBar(void)
return 42;
}

void do_start_async_method_call_one(
std::shared_ptr<sdbusplus::asio::connection> conn,
boost::asio::yield_context yield)
{
boost::system::error_code ec;
variant testValue;
conn->yield_method_call<>(yield[ec], "xyz.openbmc_project.asio-test",
"/xyz/openbmc_project/test",
"org.freedesktop.DBus.Properties", "Set",
"xyz.openbmc_project.test", "int", variant(24));
testValue = conn->yield_method_call<variant>(
yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
"org.freedesktop.DBus.Properties", "Get", "xyz.openbmc_project.test",
"int");
if (!ec && testValue.get<int>() == 24)
{
std::cout << "async call to Properties.Get serialized via yield OK!\n";
}
else
{
std::cout << "ec = " << ec << ": " << testValue.get<int>() << "\n";
}
conn->yield_method_call<void>(
yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
"org.freedesktop.DBus.Properties", "Set", "xyz.openbmc_project.test",
"int", variant(42));
testValue = conn->yield_method_call<variant>(
yield[ec], "xyz.openbmc_project.asio-test", "/xyz/openbmc_project/test",
"org.freedesktop.DBus.Properties", "Get", "xyz.openbmc_project.test",
"int");
if (!ec && testValue.get<int>() == 42)
{
std::cout << "async call to Properties.Get serialized via yield OK!\n";
}
else
{
std::cout << "ec = " << ec << ": " << testValue.get<int>() << "\n";
}
}

void do_start_async_method_call_two(
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>(
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")
{
std::cout << "async call to TestMethod serialized via yield OK!\n";
}
else
{
std::cout << "ec = " << ec << ": " << testValue << "\n";
}
}

int main()
{
using GetSubTreeType = std::vector<std::pair<
Expand Down Expand Up @@ -121,7 +185,8 @@ int main()

// test method creation
iface->register_method("TestMethod", [](const int32_t& callCount) {
return "success: " + std::to_string(callCount);
return std::make_tuple(callCount,
"success: " + std::to_string(callCount));
});

iface->register_method("TestFunction", foo);
Expand All @@ -141,6 +206,14 @@ int main()
// add the sd_event wrapper to the io object
sdbusplus::asio::sd_event_wrapper sdEvents(io);

// set up a client to make an async call to the server
// using coroutines (userspace cooperative multitasking)
boost::asio::spawn(io, [conn](boost::asio::yield_context yield) {
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);
});
io.run();

return 0;
Expand Down
97 changes: 93 additions & 4 deletions sdbusplus/asio/connection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#pragma once

#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
#include <boost/callable_traits.hpp>
#include <chrono>
#include <experimental/tuple>
Expand Down Expand Up @@ -59,10 +60,23 @@ class connection : public sdbusplus::bus::bus
socket.release();
}

/** @brief Perform an asynchronous send of a message, executing the handler
* upon return and return
*
* @param[in] m - A message ready to send
* @param[in] handler - handler to execute upon completion; this may be an
* asio::yield_context to execute asynchronously as a
* coroutine
*
* @return If a yield context is passed as the handler, the return type is
* a message. If a function object is passed in as the handler,
* the return type is the result of the handler registration,
* while the resulting message will get passed into the handler.
*/
template <typename MessageHandler>
inline BOOST_ASIO_INITFN_RESULT_TYPE(MessageHandler,
void(boost::system::error_code,
message::message))
message::message&))
async_send(message::message& m, MessageHandler&& handler)
{
boost::asio::async_completion<
Expand All @@ -75,17 +89,36 @@ class connection : public sdbusplus::bus::bus
return init.result.get();
}

/** @brief Perform an asynchronous method call, with input parameter packing
* and return value unpacking
*
* @param[in] handler - A function object that is to be called as a
* continuation for the async dbus method call. The
* arguments to parse on the return are deduced from
* the handler's signature and then passed in along
* with an error code.
* @param[in] service - The service to call.
* @param[in] objpath - The object's path for the call.
* @param[in] interf - The object's interface to call.
* @param[in] method - The object's method to call.
* @param[in] a... - Optional parameters for the method call.
*
* @return immediate return of the internal handler registration. The
* result of the actual asynchronous call will get unpacked from
* the message and passed into the handler when the call is
* complete.
*/
template <typename MessageHandler, typename... InputArgs>
auto async_method_call(MessageHandler handler, const std::string& service,
void async_method_call(MessageHandler handler, const std::string& service,
const std::string& objpath,
const std::string& interf, const std::string& method,
const InputArgs&... a)
{
message::message m = new_method_call(service.c_str(), objpath.c_str(),
interf.c_str(), method.c_str());
m.append(a...);
return async_send(m, [handler](boost::system::error_code ec,
message::message& r) {
async_send(m, [handler](boost::system::error_code ec,
message::message& r) {
using FunctionTuple =
boost::callable_traits::args_t<MessageHandler>;
using UnpackType = typename utility::strip_first_arg<
Expand All @@ -107,6 +140,62 @@ class connection : public sdbusplus::bus::bus
});
}

/** @brief Perform a yielding asynchronous method call, with input
* parameter packing and return value unpacking
*
* @param[in] yield - A yield context to async block upon. To catch errors
* for the call, call this function with 'yield[ec]',
* thus attaching an error code to the yield context
* @param[in] service - The service to call.
* @param[in] objpath - The object's path for the call.
* @param[in] interf - The object's interface to call.
* @param[in] method - The object's method to call.
* @param[in] a... - Optional parameters for the method call.
*
* @return Unpacked value of RetType
*/
template <typename... RetTypes, typename... InputArgs>
auto yield_method_call(boost::asio::yield_context yield,
const std::string& service,
const std::string& objpath,
const std::string& interf, const std::string& method,
const InputArgs&... a)
{
message::message m = new_method_call(service.c_str(), objpath.c_str(),
interf.c_str(), method.c_str());
m.append(a...);
message::message r = async_send(m, yield);
if constexpr (sizeof...(RetTypes) == 0)
{
// void return
return;
}
else if constexpr (sizeof...(RetTypes) == 1)
{
if constexpr (std::is_same<utility::first_type<RetTypes...>,
void>::value)
{
return;
}
else
{
// single item return
utility::first_type<RetTypes...> responseData;
// this will throw if the signature of r != RetType
r.read(responseData);
return responseData;
}
}
else
{
// tuple of things to return
std::tuple<RetTypes...> responseData;
// this will throw if the signature of r != RetType
r.read(responseData);
return responseData;
}
}

private:
boost::asio::io_service& io_;
boost::asio::posix::stream_descriptor socket;
Expand Down
7 changes: 7 additions & 0 deletions sdbusplus/utility/type_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ namespace sdbusplus
namespace utility
{

/** @brief Retrieve the first type from a parameter pack
*
* @tparam Types - the parameter pack
*/
template <typename... Types>
using first_type = std::tuple_element_t<0, std::tuple<Types...>>;

/** @brief Convert T[N] to T* if is_same<Tbase,T>
*
* @tparam Tbase - The base type expected.
Expand Down

0 comments on commit 261e72b

Please sign in to comment.