From a0cf8c0f7aa2be222e891d31e7710a7136a11981 Mon Sep 17 00:00:00 2001 From: ecorm Date: Wed, 23 Dec 2015 13:53:47 -0400 Subject: [PATCH] Better support for asynchronous RPC and event handlers (closes #91). Support for non-handshaking raw socket transports has been removed (closes #92). Added Variant conversion facilities for std::set and std::unordered_set. --- CHANGELOG.md | 21 + CMakeLists.txt | 6 - README.md | 2 +- cppwamp/CMakeLists.txt | 17 +- cppwamp/cppwamp.pro | 17 +- cppwamp/include/cppwamp/asyncresult.hpp | 14 + cppwamp/include/cppwamp/connector.hpp | 2 +- cppwamp/include/cppwamp/corosession.hpp | 4 +- cppwamp/include/cppwamp/corounpacker.hpp | 265 +++++++++++ .../include/cppwamp/internal/asynctask.hpp | 86 ++++ cppwamp/include/cppwamp/internal/callee.hpp | 10 +- cppwamp/include/cppwamp/internal/client.hpp | 220 +++++----- .../cppwamp/internal/clientinterface.hpp | 23 +- .../include/cppwamp/internal/corosession.ipp | 28 +- .../include/cppwamp/internal/corounpacker.ipp | 289 ++++++++++++ .../cppwamp/internal/legacyasioendpoint.hpp | 64 --- .../cppwamp/internal/legacyasiotransport.hpp | 84 ---- cppwamp/include/cppwamp/internal/passkey.hpp | 2 +- .../internal/{dialogue.hpp => peer.hpp} | 21 +- .../{dialoguedata.ipp => peerdata.ipp} | 0 cppwamp/include/cppwamp/internal/session.ipp | 77 ++-- .../include/cppwamp/internal/sessiondata.ipp | 72 ++- .../include/cppwamp/internal/subscriber.hpp | 8 +- cppwamp/include/cppwamp/internal/tcp.ipp | 11 - cppwamp/include/cppwamp/internal/uds.ipp | 11 - cppwamp/include/cppwamp/internal/unpacker.ipp | 11 +- .../{dialoguedata.hpp => peerdata.hpp} | 8 +- cppwamp/include/cppwamp/session.hpp | 30 +- cppwamp/include/cppwamp/sessiondata.hpp | 42 +- cppwamp/include/cppwamp/tcp.hpp | 19 - cppwamp/include/cppwamp/tcphost.hpp | 2 +- cppwamp/include/cppwamp/types/set.hpp | 72 +++ .../include/cppwamp/types/unorderedmap.hpp | 3 +- .../include/cppwamp/types/unorderedset.hpp | 73 ++++ cppwamp/include/cppwamp/uds.hpp | 19 - cppwamp/include/cppwamp/udspath.hpp | 2 +- cppwamp/include/cppwamp/unpacker.hpp | 6 +- cppwamp/include/cppwamp/version.hpp | 6 +- cppwamp/src/cppwamp.cpp | 8 +- doc/architecture.vpp | Bin 913408 -> 914432 bytes doc/images/client_api.svg | 34 +- doc/images/layers.svg | 262 +++++------ doc/images/messaging.svg | 208 ++++----- doc/images/registrations.svg | 54 +-- doc/images/subscriptions.svg | 42 +- doc/images/transport.svg | 412 +++++++++--------- doc/registrations.dox | 120 ++++- doc/subscriptions.dox | 97 ++++- examples/chat/main.cpp | 26 +- examples/timeclient/main.cpp | 14 +- examples/timeservice/main.cpp | 16 +- test/CMakeLists.txt | 1 - test/legacytransporttest.cpp | 325 -------------- test/test.pro | 1 - test/varianttestconvertcontainers.cpp | 146 ++++++- test/wamptest.cpp | 281 ++++++------ test/wamptestadvanced.cpp | 22 +- toolchain-arm-linux-gnueabihf.cmake | 18 + 58 files changed, 2208 insertions(+), 1526 deletions(-) create mode 100644 cppwamp/include/cppwamp/corounpacker.hpp create mode 100644 cppwamp/include/cppwamp/internal/asynctask.hpp create mode 100644 cppwamp/include/cppwamp/internal/corounpacker.ipp delete mode 100644 cppwamp/include/cppwamp/internal/legacyasioendpoint.hpp delete mode 100644 cppwamp/include/cppwamp/internal/legacyasiotransport.hpp rename cppwamp/include/cppwamp/internal/{dialogue.hpp => peer.hpp} (95%) rename cppwamp/include/cppwamp/internal/{dialoguedata.ipp => peerdata.ipp} (100%) rename cppwamp/include/cppwamp/{dialoguedata.hpp => peerdata.hpp} (94%) create mode 100644 cppwamp/include/cppwamp/types/set.hpp create mode 100644 cppwamp/include/cppwamp/types/unorderedset.hpp delete mode 100644 test/legacytransporttest.cpp create mode 100644 toolchain-arm-linux-gnueabihf.cmake diff --git a/CHANGELOG.md b/CHANGELOG.md index b024e277..abba639e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +v0.6.0 +====== +Better support for asynchronous RPC and event handlers. + +Breaking Changes: +- `Session` and `CoroSession` now take an extra `boost::asio::io_service` + argument in their `create()` functions. This IO service is now used for + executing user-provided handlers. It can be the same one used by the + transport connectors. +- Support for non-handshaking raw socket transports has been + removed (closes #92). + +Enhancements: +- Added `basicCoroRpc()`, `basicCoroEvent()`, `unpackedCoroRpc()`, and + `unpackedCoroEvent()` wrappers, which execute a call/event slot within the + context of a coroutine. This should make it easier to implement RPC/event + handlers that need to run asynchronously themselves (closes #91). +- `Invocation` and `Event` now have an `iosvc()` getter, which returns the + user-provided `asio::io_service` (closes #91). +- Added `Variant` conversion facilities for `std::set` and `set::unordered_set`. + v0.5.3 ====== Fixes and enhancements. diff --git a/CMakeLists.txt b/CMakeLists.txt index 78c5d5b3..71c2238a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,12 +37,6 @@ set(PATH_INCLUDE_MSGPACK ${PROJECT_SOURCE_DIR}/ext/msgpack-c/include CACHE PATH set(PATH_INCLUDE_CATCH ${PROJECT_SOURCE_DIR}/ext/Catch/include CACHE PATH "Catch include path") -# Add GUI variables that let the user specify that legacy connectors should be -# used for tests and examples. -option(CPPWAMP_USE_NON_HANDSHAKING_TRANSPORTS - "Use non-handshaking raw socket transports in tests and examples" - OFF) - # Confirm that the user's choices for the Boost library paths are valid. unset(BOOST_ROOT) set(BOOST_INCLUDEDIR ${PATH_INCLUDE_BOOST}) diff --git a/README.md b/README.md index dddef666..c56c6f48 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { // Specify a TCP transport and JSON serialization auto tcp = connector(iosvc, TcpHost("localhost", 8001)); - auto session = wamp::CoroSession<>::create(tcp); + auto session = wamp::CoroSession<>::create(iosvc, tcp); session->connect(yield); auto sessionInfo = session->join(Realm("myrealm"), yield); std::cout << "Client joined. Session ID = " diff --git a/cppwamp/CMakeLists.txt b/cppwamp/CMakeLists.txt index 62fc8768..9dfb044e 100644 --- a/cppwamp/CMakeLists.txt +++ b/cppwamp/CMakeLists.txt @@ -15,13 +15,14 @@ set(HEADERS include/cppwamp/connector.hpp include/cppwamp/conversion.hpp include/cppwamp/corosession.hpp - include/cppwamp/dialoguedata.hpp + include/cppwamp/corounpacker.hpp include/cppwamp/error.hpp include/cppwamp/json.hpp include/cppwamp/msgpack.hpp include/cppwamp/null.hpp include/cppwamp/options.hpp include/cppwamp/payload.hpp + include/cppwamp/peerdata.hpp include/cppwamp/rawsockoptions.hpp include/cppwamp/registration.hpp include/cppwamp/session.hpp @@ -37,20 +38,21 @@ set(HEADERS include/cppwamp/version.hpp include/cppwamp/visitor.hpp include/cppwamp/wampdefs.hpp + include/cppwamp/internal/asynctask.hpp include/cppwamp/internal/asioconnector.hpp include/cppwamp/internal/asioendpoint.hpp include/cppwamp/internal/asiolistener.hpp include/cppwamp/internal/asiotransport.hpp + include/cppwamp/internal/base64.hpp include/cppwamp/internal/callee.hpp include/cppwamp/internal/client.hpp include/cppwamp/internal/clientinterface.hpp include/cppwamp/internal/config.hpp - include/cppwamp/internal/dialogue.hpp include/cppwamp/internal/endian.hpp include/cppwamp/internal/integersequence.hpp - include/cppwamp/internal/legacyasioendpoint.hpp - include/cppwamp/internal/legacyasiotransport.hpp include/cppwamp/internal/messagetraits.hpp + include/cppwamp/internal/passkey.hpp + include/cppwamp/internal/peer.hpp include/cppwamp/internal/precompiled.hpp include/cppwamp/internal/rawsockconnector.hpp include/cppwamp/internal/rawsockhandshake.hpp @@ -65,18 +67,19 @@ set(HEADERS include/cppwamp/internal/varianttraitsfwd.hpp include/cppwamp/internal/variantvisitors.hpp include/cppwamp/internal/wampmessage.hpp + include/cppwamp/types/set.hpp include/cppwamp/types/tuple.hpp include/cppwamp/types/boostoptional.hpp include/cppwamp/types/unorderedmap.hpp + include/cppwamp/types/unorderedset.hpp ) set(INLINES include/cppwamp/internal/asyncresult.ipp - include/cppwamp/internal/base64.hpp include/cppwamp/internal/blob.ipp include/cppwamp/internal/conversion.ipp include/cppwamp/internal/corosession.ipp - include/cppwamp/internal/dialoguedata.ipp + include/cppwamp/internal/corounpacker.ipp include/cppwamp/internal/endian.ipp include/cppwamp/internal/error.ipp include/cppwamp/internal/json.ipp @@ -84,8 +87,8 @@ set(INLINES include/cppwamp/internal/msgpack.ipp include/cppwamp/internal/null.ipp include/cppwamp/internal/options.ipp - include/cppwamp/internal/passkey.hpp include/cppwamp/internal/payload.ipp + include/cppwamp/internal/peerdata.ipp include/cppwamp/internal/rawsockoptions.ipp include/cppwamp/internal/registration.ipp include/cppwamp/internal/session.ipp diff --git a/cppwamp/cppwamp.pro b/cppwamp/cppwamp.pro index 126fed6c..8c215734 100644 --- a/cppwamp/cppwamp.pro +++ b/cppwamp/cppwamp.pro @@ -20,13 +20,14 @@ HEADERS += \ include/cppwamp/connector.hpp \ include/cppwamp/conversion.hpp \ include/cppwamp/corosession.hpp \ - include/cppwamp/dialoguedata.hpp \ + include/cppwamp/corounpacker.hpp \ include/cppwamp/error.hpp \ include/cppwamp/json.hpp \ include/cppwamp/msgpack.hpp \ include/cppwamp/null.hpp \ include/cppwamp/options.hpp \ include/cppwamp/payload.hpp \ + include/cppwamp/peerdata.hpp \ include/cppwamp/rawsockoptions.hpp \ include/cppwamp/registration.hpp \ include/cppwamp/session.hpp \ @@ -42,20 +43,21 @@ HEADERS += \ include/cppwamp/version.hpp \ include/cppwamp/visitor.hpp \ include/cppwamp/wampdefs.hpp \ + include/cppwamp/internal/asynctask.hpp \ include/cppwamp/internal/asioconnector.hpp \ include/cppwamp/internal/asioendpoint.hpp \ include/cppwamp/internal/asiolistener.hpp \ include/cppwamp/internal/asiotransport.hpp \ + include/cppwamp/internal/base64.hpp \ include/cppwamp/internal/callee.hpp \ include/cppwamp/internal/client.hpp \ include/cppwamp/internal/clientinterface.hpp \ include/cppwamp/internal/config.hpp \ - include/cppwamp/internal/dialogue.hpp \ include/cppwamp/internal/endian.hpp \ include/cppwamp/internal/integersequence.hpp \ - include/cppwamp/internal/legacyasioendpoint.hpp \ - include/cppwamp/internal/legacyasiotransport.hpp \ include/cppwamp/internal/messagetraits.hpp \ + include/cppwamp/internal/passkey.hpp \ + include/cppwamp/internal/peer.hpp \ include/cppwamp/internal/precompiled.hpp \ include/cppwamp/internal/rawsockconnector.hpp \ include/cppwamp/internal/rawsockhandshake.hpp \ @@ -70,16 +72,17 @@ HEADERS += \ include/cppwamp/internal/varianttraitsfwd.hpp \ include/cppwamp/internal/variantvisitors.hpp \ include/cppwamp/internal/wampmessage.hpp \ + include/cppwamp/types/set.hpp \ include/cppwamp/types/tuple.hpp \ include/cppwamp/types/boostoptional.hpp \ include/cppwamp/types/unorderedmap.hpp \ + include/cppwamp/types/unorderedset.hpp \ \ include/cppwamp/internal/asyncresult.ipp \ - include/cppwamp/internal/base64.hpp \ include/cppwamp/internal/blob.ipp \ include/cppwamp/internal/conversion.ipp \ include/cppwamp/internal/corosession.ipp \ - include/cppwamp/internal/dialoguedata.ipp \ + include/cppwamp/internal/corounpacker.ipp \ include/cppwamp/internal/endian.ipp \ include/cppwamp/internal/error.ipp \ include/cppwamp/internal/json.ipp \ @@ -87,8 +90,8 @@ HEADERS += \ include/cppwamp/internal/msgpack.ipp \ include/cppwamp/internal/null.ipp \ include/cppwamp/internal/options.ipp \ - include/cppwamp/internal/passkey.hpp \ include/cppwamp/internal/payload.ipp \ + include/cppwamp/internal/peerdata.ipp \ include/cppwamp/internal/rawsockoptions.ipp \ include/cppwamp/internal/registration.ipp \ include/cppwamp/internal/session.ipp \ diff --git a/cppwamp/include/cppwamp/asyncresult.hpp b/cppwamp/include/cppwamp/asyncresult.hpp index 2a45996a..55491920 100644 --- a/cppwamp/include/cppwamp/asyncresult.hpp +++ b/cppwamp/include/cppwamp/asyncresult.hpp @@ -87,6 +87,20 @@ class AsyncResult //------------------------------------------------------------------------------ template using AsyncHandler = std::function)>; + +//------------------------------------------------------------------------------ +/** Type traits template used to obtain the result type of an asynchronous + handler. */ +//------------------------------------------------------------------------------ +template +struct ResultTypeOfHandler {}; + +//------------------------------------------------------------------------------ +/** ResultTypeOfHandler specialization for AsyncHandler */ +//------------------------------------------------------------------------------ +template +struct ResultTypeOfHandler> {using Type = AsyncResult;}; + } // namespace wamp #include "internal/asyncresult.ipp" diff --git a/cppwamp/include/cppwamp/connector.hpp b/cppwamp/include/cppwamp/connector.hpp index 3cb20176..3d8dce6d 100644 --- a/cppwamp/include/cppwamp/connector.hpp +++ b/cppwamp/include/cppwamp/connector.hpp @@ -32,7 +32,7 @@ namespace internal {class ClientInterface;} The Session class uses these Connector objects when attempting to establish a connection to the router. - @see connector, legacyConnector */ + @see connector */ //------------------------------------------------------------------------------ class Connector : public std::enable_shared_from_this { diff --git a/cppwamp/include/cppwamp/corosession.hpp b/cppwamp/include/cppwamp/corosession.hpp index 621a5c3c..cc98b403 100644 --- a/cppwamp/include/cppwamp/corosession.hpp +++ b/cppwamp/include/cppwamp/corosession.hpp @@ -113,10 +113,10 @@ class CoroSession : public TBase using YieldContext = boost::asio::basic_yield_context; /** Creates a new CoroSession instance. */ - static Ptr create(const Connector::Ptr& connector); + static Ptr create(AsioService& userIosvc, const Connector::Ptr& connector); /** Creates a new CoroSession instance. */ - static Ptr create(const ConnectorList& connectors); + static Ptr create(AsioService& userIosvc, const ConnectorList& connectors); using Base::connect; using Base::join; diff --git a/cppwamp/include/cppwamp/corounpacker.hpp b/cppwamp/include/cppwamp/corounpacker.hpp new file mode 100644 index 00000000..60f86e41 --- /dev/null +++ b/cppwamp/include/cppwamp/corounpacker.hpp @@ -0,0 +1,265 @@ +/*------------------------------------------------------------------------------ + Copyright Butterfly Energy Systems 2014-2015. + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at + http://www.boost.org/LICENSE_1_0.txt) +------------------------------------------------------------------------------*/ + +#ifndef CPPWAMP_COROUNPACKER_HPP +#define CPPWAMP_COROUNPACKER_HPP + +//------------------------------------------------------------------------------ +/** @file + Contains utilities for unpacking positional arguments passed to + event slots and call slots that spawn coroutines. */ +//------------------------------------------------------------------------------ + +#include +#include +#include "unpacker.hpp" + +namespace wamp +{ + +//------------------------------------------------------------------------------ +/** Wrapper around an event coroutine slot which automatically unpacks + positional payload arguments. + The [wamp::unpackedCoroEvent](@ref CoroEventUnpacker::unpackedCoroEvent) + convenience function should be used to construct instances of + CoroEventUnpacker. + @see [wamp::unpackedCoroEvent](@ref CoroEventUnpacker::unpackedCoroEvent) + @see @ref UnpackedCoroutineEventSlots + @tparam TSlot Function type to be wrapped. Must have the signature + `void function(Event, TArgs..., boost::asio::yield_context)`. + @tparam TArgs List of static types the event slot expects following the + Event parameter and preceding the `boost::asio::yield_context` + parameter. */ +//------------------------------------------------------------------------------ +template +class CoroEventUnpacker +{ +public: + /// The function type to be wrapped. + using Slot = TSlot; + + /** Constructor taking a callable target. */ + explicit CoroEventUnpacker(Slot slot); + + /** Spawns a new coroutine and executes the stored event slot. + The coroutine will be spawned using `event.iosvc()`. + The `event.args()` positional arguments will be unpacked and passed + to the stored event slot as additional parameters. */ + void operator()(Event&& event); + +private: + using Yield = boost::asio::yield_context; + + template + void invoke(Event&& event, internal::IntegerSequence); + + std::shared_ptr slot_; +}; + +//------------------------------------------------------------------------------ +/** @relates CoroEventUnpacker + Converts an unpacked event slot into a regular slot than can be passed + to Session::subscribe. + The slot will be executed within the context of a coroutine and will be + given a `boost::asio::yield_context` as the last call argument. + @see @ref UnpackedCoroutineEventSlots + @returns An CoroEventUnpacker that wraps the the given slot. + @tparam TArgs List of static types the event slot expects following the + Event parameter, and preceding the + `boost::asio::yield_context` parameter. + @tparam TSlot (deduced) Function type to be converted. Must have the signature + `void function(Event, TArgs..., boost::asio::yield_context)`. */ +//------------------------------------------------------------------------------ +template +CoroEventUnpacker, TArgs...> unpackedCoroEvent(TSlot&& slot); + + +//------------------------------------------------------------------------------ +/** Wrapper around an event slot which automatically unpacks positional + payload arguments. + The [wamp::basicCoroEvent](@ref BasicCoroEventUnpacker::basicCoroEvent) + convenience function should be used to construct instances of + BasicCoroEventUnpacker. + This class differs from CoroEventUnpacker in that the slot type is not + expected to take an Event as the first parameter. + @see [wamp::basicCoroEvent](@ref BasicCoroEventUnpacker::basicCoroEvent) + @see @ref BasicCoroutineEventSlots + @tparam TSlot Function type to be wrapped. Must have the signature + `void function(TArgs..., boost::asio::yield_context)`. + @tparam TArgs List of static types the event slot expects as arguments + preceding the `boost::asio::yield_context` parameter. */ +//------------------------------------------------------------------------------ +template +class BasicCoroEventUnpacker +{ +public: + /// The function type to be wrapped. + using Slot = TSlot; + + /** Constructor taking a callable target. */ + explicit BasicCoroEventUnpacker(Slot slot); + + /** Spawns a new coroutine and executes the stored event slot. + The coroutine will be spawned using `event.iosvc()`. + The `event.args()` positional arguments will be unpacked and passed + to the stored event slot as parameters. */ + void operator()(Event&& event); + +private: + using Yield = boost::asio::yield_context; + + template + void invoke(Event&& event, internal::IntegerSequence); + + std::shared_ptr slot_; +}; + +//------------------------------------------------------------------------------ +/** @relates BasicCoroEventUnpacker + Converts an unpacked event slot into a regular slot than can be passed + to Session::subscribe. + This function differs from `unpackedCoroEvent` in that the slot type is not + expected to take an Event as the first parameter. + @see @ref BasicCoroutineEventSlots + @returns An BasicCoroEventUnpacker that wraps the the given slot. + @tparam TArgs List of static types the event slot expects as arguments, + preceding the `boost::asio::yield_context` parameter. + @tparam TSlot (deduced) Function type to be converted. Must have the + signature `void function(TArgs..., boost::asio::yield_context)`.*/ +//------------------------------------------------------------------------------ +template +BasicCoroEventUnpacker, TArgs...> +basicCoroEvent(TSlot&& slot); + + +//------------------------------------------------------------------------------ +/** Wrapper around a call coroutine slot which automatically unpacks positional + payload arguments. + The [wamp::unpackedCoroRpc](@ref CoroInvocationUnpacker::unpackedCoroRpc) + convenience function should be used to construct instances of + CoroInvocationUnpacker. + @see [wamp::unpackedCoroRpc](@ref CoroInvocationUnpacker::unpackedCoroRpc) + @see @ref UnpackedCoroutineCallSlots + @tparam TSlot Function type to be wrapped. Must have the signature + `void function(Invocation, TArgs..., boost::asio::yield_context)`. + @tparam TArgs List of static types the call slot expects following the + Invocation parameter, and preceding the boost::asio::yield_context + parameter. */ +//------------------------------------------------------------------------------ +template +class CoroInvocationUnpacker +{ +public: + /// The function type to be wrapped. + using Slot = TSlot; + + /** Constructor taking a callable target. */ + explicit CoroInvocationUnpacker(Slot slot); + + /** Spawns a new coroutine and executes the stored call slot. + The coroutine will be spawned using `inv.iosvc()`. + The `inv.args()` positional arguments will be unpacked and passed + to the stored call slot as additional parameters. */ + Outcome operator()(Invocation&& inv); + +private: + using Yield = boost::asio::yield_context; + + template + void invoke(Invocation&& inv, internal::IntegerSequence); + + std::shared_ptr slot_; +}; + +//------------------------------------------------------------------------------ +/** @relates CoroInvocationUnpacker + Converts an unpacked call slot into a regular slot than can be passed + to Session::enroll. + @see @ref UnpackedCoroutineCallSlots + @returns A CoroInvocationUnpacker that wraps the the given slot. + @tparam TArgs List of static types the call slot expects following the + Invocation parameter, and preceding the + `boost::asio::yield_context` parameter. + @tparam TSlot (deduced) Function type to be converted. Must have the signature + `Outcome function(Invocation, TArgs..., boost::asio::yield_context)`.*/ +//------------------------------------------------------------------------------ +template +CoroInvocationUnpacker, TArgs...> +unpackedCoroRpc(TSlot&& slot); + + +//------------------------------------------------------------------------------ +/** Wrapper around a call slot which automatically unpacks positional payload + arguments. + The [wamp::basicCoroRpc](@ref BasicCoroInvocationUnpacker::basicCoroRpc) + convenience function should be used to construct instances of + BasicCoroInvocationUnpacker. + This class differs from CoroInvocationUnpacker in that the slot type returns + `TResult` and is not expected to take an Invocation as the first parameter. + @see [wamp::basicCoroRpc](@ref BasicCoroInvocationUnpacker::basicCoroRpc) + @see @ref BasicCoroutineCallSlots + @tparam TSlot Function type to be wrapped. Must have the signature + `TResult function(TArgs..., boost::asio::yield_context)`. + @tparam TResult The static result type returned by the slot (may be `void`). + @tparam TArgs List of static types the call slot expects as arguments, + preceding the `boost::asio::yield_context` argument. */ +//------------------------------------------------------------------------------ +template +class BasicCoroInvocationUnpacker +{ +public: + /// The function type to be wrapped. + using Slot = TSlot; + + /// The static result type returned by the slot. + using ResultType = TResult; + + /** Constructor taking a callable target. */ + explicit BasicCoroInvocationUnpacker(Slot slot); + + /** Spawns a new coroutine and executes the stored call slot. + The coroutine will be spawned using `inv.iosvc()`. + The `inv.args()` positional arguments will be unpacked and passed + to the stored call slot as additional parameters. */ + Outcome operator()(Invocation&& inv); + +private: + using Yield = boost::asio::yield_context; + + template + void invoke(TrueType, Invocation&& inv, internal::IntegerSequence); + + template + void invoke(FalseType, Invocation&& inv, internal::IntegerSequence); + + std::shared_ptr slot_; +}; + +//------------------------------------------------------------------------------ +/** @relates BasicCoroInvocationUnpacker + Converts an unpacked call slot into a regular slot than can be passed + to Session::enroll. + This function differs from `unpackedCoroRpc` in that the slot type returns + TResult and is not expected to take an Invocation as the first parameter. + @see @ref BasicCoroutineCallSlots + @returns A BasicCoroInvocationUnpacker that wraps the the given slot. + @tparam TArgs List of static types the call slot expects as arguments, + preceding the `boost::asio::yield_context` argument. + @tparam TResult The static result type returned by the slot (may be `void`). + @tparam TSlot (deduced) Function type to be converted. Must have the signature + `TResult function(TArgs..., boost::asio::yield_context)`.*/ +//------------------------------------------------------------------------------ +template +BasicCoroInvocationUnpacker, TResult, TArgs...> +basicCoroRpc(TSlot&& slot); + + +} // namespace wamp + +#include "./internal/corounpacker.ipp" + +#endif // CPPWAMP_COROUNPACKER_HPP diff --git a/cppwamp/include/cppwamp/internal/asynctask.hpp b/cppwamp/include/cppwamp/internal/asynctask.hpp new file mode 100644 index 00000000..d89cc8ff --- /dev/null +++ b/cppwamp/include/cppwamp/internal/asynctask.hpp @@ -0,0 +1,86 @@ +/*------------------------------------------------------------------------------ + Copyright Butterfly Energy Systems 2014-2015. + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at + http://www.boost.org/LICENSE_1_0.txt) +------------------------------------------------------------------------------*/ + +#ifndef CPPWAMP_ASYNCTASK_HPP +#define CPPWAMP_ASYNCTASK_HPP + +#include +#include +#include "../asiodefs.hpp" +#include "../asyncresult.hpp" + +namespace wamp +{ + +//------------------------------------------------------------------------------ +// Bundles an AsyncHandler along with the AsioService in which the handler +// is to be posted. +//------------------------------------------------------------------------------ +template +class AsyncTask +{ +public: + using ValueType = TResult; + + AsyncTask() : iosvc_(nullptr) {} + + AsyncTask(AsioService& iosvc, AsyncHandler handler) + : iosvc_(&iosvc), + handler_(std::move(handler)) + {} + + AsyncTask(const AsyncTask& other) = default; + + AsyncTask(AsyncTask&& other) noexcept + : iosvc_(other.iosvc_), + handler_(std::move(other.handler_)) + { + other.iosvc_ = nullptr; + } + + AsyncTask& operator=(const AsyncTask& other) = default; + + AsyncTask& operator=(AsyncTask&& other) noexcept + { + iosvc_ = other.iosvc_; + handler_ = std::move(other.handler_); + other.iosvc_ = nullptr; + return *this; + } + + explicit operator bool() const {return iosvc_ != nullptr;} + + AsioService& iosvc() const {return *iosvc_;} + + const AsyncHandler& handler() const {return handler_;} + + void operator()(AsyncResult result) const & + { + assert(iosvc_ && "Invoking uninitialized AsyncTask"); + iosvc_->post(std::bind(handler_, std::move(result))); + } + + void operator()(AsyncResult result) && + { + assert(iosvc_ && "Invoking uninitialized AsyncTask"); + iosvc_->post(std::bind(std::move(handler_), std::move(result))); + } + +private: + AsioService* iosvc_; + AsyncHandler handler_; +}; + +//------------------------------------------------------------------------------ +/** ResultTypeOfHandler specialization for AsyncTask */ +//------------------------------------------------------------------------------ +template +struct ResultTypeOfHandler> {using Type = AsyncResult;}; + +} // namespace wamp + +#endif // CPPWAMP_ASYNCTASK_HPP diff --git a/cppwamp/include/cppwamp/internal/callee.hpp b/cppwamp/include/cppwamp/internal/callee.hpp index 1dbee0f3..8cc1896a 100644 --- a/cppwamp/include/cppwamp/internal/callee.hpp +++ b/cppwamp/include/cppwamp/internal/callee.hpp @@ -11,10 +11,10 @@ #include #include #include -#include "../asyncresult.hpp" -#include "../dialoguedata.hpp" +#include "../peerdata.hpp" #include "../sessiondata.hpp" #include "../wampdefs.hpp" +#include "asynctask.hpp" namespace wamp { @@ -32,10 +32,10 @@ class Callee virtual ~Callee() {} - virtual void unregister(const Registration& handle) = 0; + virtual void unregister(const Registration& reg) = 0; - virtual void unregister(const Registration& handle, - AsyncHandler handler) = 0; + virtual void unregister(const Registration& reg, + AsyncTask&& handler) = 0; virtual void yield(RequestId reqId, wamp::Result&& result) = 0; diff --git a/cppwamp/include/cppwamp/internal/client.hpp b/cppwamp/include/cppwamp/internal/client.hpp index 66f7881d..cf9ff449 100644 --- a/cppwamp/include/cppwamp/internal/client.hpp +++ b/cppwamp/include/cppwamp/internal/client.hpp @@ -9,7 +9,6 @@ #define CPPWAMP_INTERNAL_CLIENT_HPP #include -#include #include #include #include @@ -17,6 +16,7 @@ #include #include #include +#include #include #include #include "../registration.hpp" @@ -24,7 +24,7 @@ #include "../unpacker.hpp" #include "../version.hpp" #include "clientinterface.hpp" -#include "dialogue.hpp" +#include "peer.hpp" namespace wamp { @@ -36,7 +36,7 @@ namespace internal // Provides the implementation of the wamp::Session class. //------------------------------------------------------------------------------ template -class Client : public ClientInterface, public Dialogue +class Client : public ClientInterface, public Peer { public: using Ptr = std::shared_ptr; @@ -53,7 +53,7 @@ class Client : public ClientInterface, public Dialogue virtual State state() const override {return Base::state();} - virtual void join(Realm&& realm, AsyncHandler handler) override + virtual void join(Realm&& realm, AsyncTask&& handler) override { using std::move; @@ -73,11 +73,10 @@ class Client : public ClientInterface, public Dialogue { if (reply.type == WampMsgType::welcome) { - this->post(handler, - SessionInfo({}, - move(realmUri), - move(reply.as(1)), - move(reply.as(2)))); + move(handler)(SessionInfo({}, + move(realmUri), + move(reply.as(1)), + move(reply.as(2)))); } else { @@ -93,14 +92,13 @@ class Client : public ClientInterface, public Dialogue AsyncResult result(make_error_code(errc), oss.str()); - this->post(handler, move(result)); + move(handler)(move(result)); } } }); } - virtual void leave(Reason&& reason, - AsyncHandler&& handler) override + virtual void leave(Reason&& reason, AsyncTask&& handler) override { using std::move; if (reason.uri().empty()) @@ -113,7 +111,7 @@ class Client : public ClientInterface, public Dialogue { auto reason = Reason(move(reply.as(2))) .withOptions(move(reply.as(1))); - this->post(handler, move(reason)); + move(handler)(move(reason)); } readership_.clear(); registry_.clear(); @@ -127,34 +125,35 @@ class Client : public ClientInterface, public Dialogue virtual void terminate() override { - setLogHandlers(nullptr, nullptr); + using Handler = AsyncTask; + setLogHandlers(Handler(), Handler()); this->close(true); } virtual void subscribe(Topic&& topic, EventSlot&& slot, - AsyncHandler handler) override + AsyncTask&& handler) override { using std::move; - SubscriptionRecord sub = {move(topic), move(slot)}; + SubscriptionRecord rec = {move(topic), move(slot), handler.iosvc()}; - auto kv = topics_.find(sub.topic.uri()); + auto kv = topics_.find(rec.topic.uri()); if (kv == topics_.end()) { - subscribeMsg_.at(2) = sub.topic.options(); - subscribeMsg_.at(3) = sub.topic.uri(); + subscribeMsg_.at(2) = rec.topic.options(); + subscribeMsg_.at(3) = rec.topic.uri(); auto self = this->shared_from_this(); this->request(subscribeMsg_, - [this, self, sub, handler](std::error_code ec, Message reply) + [this, self, rec, handler](std::error_code ec, Message reply) { if (checkReply(WampMsgType::subscribed, ec, reply, SessionErrc::subscribeError, handler)) { auto subId = reply.to(2); auto slotId = nextSlotId(); - Subscription handle(self, subId, slotId, {}); - topics_.emplace(sub.topic.uri(), subId); - readership_[subId][slotId] = move(sub); - this->post(handler, move(handle)); + Subscription sub(self, subId, slotId, {}); + topics_.emplace(rec.topic.uri(), subId); + readership_[subId][slotId] = move(rec); + std::move(handler)(std::move(sub)); } }); } @@ -162,21 +161,21 @@ class Client : public ClientInterface, public Dialogue { auto subId = kv->second; auto slotId = nextSlotId(); - Subscription handle{this->shared_from_this(), subId, slotId, {}}; - readership_[subId][slotId] = move(sub); - this->post(handler, move(handle)); + Subscription sub{this->shared_from_this(), subId, slotId, {}}; + readership_[subId][slotId] = move(rec); + std::move(handler)(move(sub)); } } - virtual void unsubscribe(const Subscription& handle) override + virtual void unsubscribe(const Subscription& sub) override { - auto kv = readership_.find(handle.id()); + auto kv = readership_.find(sub.id()); if (kv != readership_.end()) { auto& subMap = kv->second; if (!subMap.empty()) { - auto subKv = subMap.find(handle.slotId({})); + auto subKv = subMap.find(sub.slotId({})); if (subKv != subMap.end()) { if (subMap.size() == 1u) @@ -184,23 +183,23 @@ class Client : public ClientInterface, public Dialogue subMap.erase(subKv); if (subMap.empty()) - sendUnsubscribe(handle.id()); + sendUnsubscribe(sub.id()); } } } } - virtual void unsubscribe(const Subscription& handle, - AsyncHandler handler) override + virtual void unsubscribe(const Subscription& sub, + AsyncTask&& handler) override { bool unsubscribed = false; - auto kv = readership_.find(handle.id()); + auto kv = readership_.find(sub.id()); if (kv != readership_.end()) { auto& subMap = kv->second; if (!subMap.empty()) { - auto subKv = subMap.find(handle.slotId({})); + auto subKv = subMap.find(sub.slotId({})); if (subKv != subMap.end()) { unsubscribed = true; @@ -210,15 +209,15 @@ class Client : public ClientInterface, public Dialogue subMap.erase(subKv); if (subMap.empty()) { - sendUnsubscribe(handle.id(), std::move(handler)); - handler = nullptr; + sendUnsubscribe(sub.id(), std::move(handler)); + handler = AsyncTask(); } } } } if (handler) - this->post(handler, unsubscribed); + std::move(handler)(unsubscribed); } virtual void publish(Pub&& pub) override @@ -226,53 +225,52 @@ class Client : public ClientInterface, public Dialogue this->send(marshallPublish(std::move(pub))); } - virtual void publish(Pub&& pub, - AsyncHandler&& handler) override + virtual void publish(Pub&& pub, AsyncTask&& handler) override { pub.options({}).emplace("acknowledge", true); auto self = this->shared_from_this(); this->request(marshallPublish(std::move(pub)), [this, self, handler](std::error_code ec, Message reply) { - if (checkReply(WampMsgType::published, ec, reply, - SessionErrc::publishError, handler)) + if (checkReply(WampMsgType::published, ec, reply, + SessionErrc::publishError, handler)) { - this->post(handler, reply.to(2)); + std::move(handler)(reply.to(2)); } }); } virtual void enroll(Procedure&& procedure, CallSlot&& slot, - AsyncHandler&& handler) override + AsyncTask&& handler) override { using std::move; - RegistrationRecord reg{move(procedure), move(slot)}; - enrollMsg_.at(2) = reg.procedure.options(); - enrollMsg_.at(3) = reg.procedure.uri(); + RegistrationRecord rec{move(procedure), move(slot), handler.iosvc()}; + enrollMsg_.at(2) = rec.procedure.options(); + enrollMsg_.at(3) = rec.procedure.uri(); auto self = this->shared_from_this(); this->request(enrollMsg_, - [this, self, reg, handler](std::error_code ec, Message reply) + [this, self, rec, handler](std::error_code ec, Message reply) { - if (checkReply(WampMsgType::registered, ec, - reply, SessionErrc::registerError, handler)) + if (checkReply(WampMsgType::registered, ec, reply, + SessionErrc::registerError, handler)) { auto regId = reply.to(2); - Registration handle(self, regId, {}); - registry_[regId] = move(reg); - this->post(handler, move(handle)); + Registration reg(self, regId, {}); + registry_[regId] = move(rec); + move(handler)(move(reg)); } }); } - virtual void unregister(const Registration& handle) override + virtual void unregister(const Registration& reg) override { - auto kv = registry_.find(handle.id()); + auto kv = registry_.find(reg.id()); if (kv != registry_.end()) { registry_.erase(kv); if (state() == State::established) { - unregisterMsg_.at(2) = handle.id(); + unregisterMsg_.at(2) = reg.id(); auto self = this->shared_from_this(); this->request( unregisterMsg_, [this, self](std::error_code ec, Message reply) @@ -286,30 +284,30 @@ class Client : public ClientInterface, public Dialogue } } - virtual void unregister(const Registration& handle, - AsyncHandler handler) override + virtual void unregister(const Registration& reg, + AsyncTask&& handler) override { CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); - auto kv = registry_.find(handle.id()); + auto kv = registry_.find(reg.id()); if (kv != registry_.end()) { registry_.erase(kv); - unregisterMsg_.at(2) = handle.id(); + unregisterMsg_.at(2) = reg.id(); auto self = this->shared_from_this(); this->request( unregisterMsg_, [this, self, handler](std::error_code ec, Message reply) { if (checkReply(WampMsgType::unregistered, ec, reply, SessionErrc::unregisterError, handler)) - this->post(handler, true); + std::move(handler)(true); }); } else - this->post(handler, false); + std::move(handler)(false); } - virtual void call(Rpc&& rpc, AsyncHandler&& handler) override + virtual void call(Rpc&& rpc, AsyncTask&& handler) override { using std::move; Error* errorPtr = rpc.error({}); @@ -369,48 +367,48 @@ class Client : public ClientInterface, public Dialogue this->sendError(WampMsgType::invocation, reqId, move(failure)); } - virtual void setLogHandlers(LogHandler warningHandler, - LogHandler traceHandler) override + virtual void setLogHandlers(AsyncTask warningHandler, + AsyncTask traceHandler) override { warningHandler_ = std::move(warningHandler); this->setTraceHandler(std::move(traceHandler)); } - virtual void postpone(std::function functor) override - { - this->post(functor); - } - private: struct SubscriptionRecord { using Slot = std::function; - SubscriptionRecord() : topic("") {} + SubscriptionRecord() : topic(""), iosvc(nullptr) {} - SubscriptionRecord(Topic&& topic, Slot&& slot) - : topic(std::move(topic)), slot(std::move(slot)) + SubscriptionRecord(Topic&& topic, Slot&& slot, AsioService& iosvc) + : topic(std::move(topic)), slot(std::move(slot)), iosvc(&iosvc) {} Topic topic; Slot slot; + AsioService* iosvc; }; struct RegistrationRecord { using Slot = std::function; - RegistrationRecord() : procedure("") {} + RegistrationRecord() : procedure(""), iosvc(nullptr) {} - RegistrationRecord(Procedure&& procedure, Slot&& slot) - : procedure(std::move(procedure)), slot(std::move(slot)) + RegistrationRecord(Procedure&& procedure, Slot&& slot, + AsioService& iosvc) + : procedure(std::move(procedure)), + slot(std::move(slot)), + iosvc(&iosvc) {} Procedure procedure; Slot slot; + AsioService* iosvc = nullptr; }; - using Base = Dialogue; + using Base = Peer; using WampMsgType = internal::WampMsgType; using Message = internal::WampMessage; using SlotId = uint64_t; @@ -422,11 +420,6 @@ class Client : public ClientInterface, public Dialogue Client(TransportPtr transport) : Base(std::move(transport)) { - warningHandler_ = [](const std::string& log) - { - std::cerr << "[CppWAMP] Warning: " << log << "\n"; - }; - initMessages(); } @@ -452,7 +445,7 @@ class Client : public ClientInterface, public Dialogue } } - void sendUnsubscribe(SubscriptionId subId, AsyncHandler&& handler) + void sendUnsubscribe(SubscriptionId subId, AsyncTask&& handler) { CPPWAMP_LOGIC_CHECK((this->state() == State::established), "Session is not established"); @@ -463,7 +456,7 @@ class Client : public ClientInterface, public Dialogue { if (checkReply(WampMsgType::unsubscribed, ec, reply, SessionErrc::unsubscribeError, handler)) - this->post(handler, true); + std::move(handler)(true); }); } @@ -494,7 +487,7 @@ class Client : public ClientInterface, public Dialogue } void callProcedure(Message& msg, Error* errorPtr, - AsyncHandler&& handler) + AsyncTask&& handler) { auto self = this->shared_from_this(); this->request(msg, @@ -509,8 +502,8 @@ class Client : public ClientInterface, public Dialogue errorPtr->withKwargs(reply.as(6)); } - if (checkReply(WampMsgType::result, ec, reply, - SessionErrc::callError, handler)) + if (checkReply(WampMsgType::result, ec, reply, + SessionErrc::callError, handler)) { using std::move; Result result({}, reply.to(1), @@ -520,7 +513,7 @@ class Client : public ClientInterface, public Dialogue result.withArgList(move(reply.as(3))); if (reply.size() >= 5) result.withKwargs(move(reply.as(4))); - this->post(handler, std::move(result)); + move(handler)(move(result)); } }); } @@ -551,21 +544,26 @@ class Client : public ClientInterface, public Dialogue { using std::move; - Event event({}, - msg.to(1), - msg.to(2), - move(msg.as(3))); + auto subId = msg.to(1); + auto pubId = msg.to(2); - auto kv = readership_.find(event.subId()); + auto kv = readership_.find(subId); if (kv != readership_.end()) { + const auto& localSubs = kv->second; + assert(!localSubs.empty()); + Event event({}, + msg.to(1), + msg.to(2), + localSubs.begin()->second.iosvc, + move(msg.as(3))); + if (msg.fields.size() >= 5) event.args({}) = move(msg.as(4)); if (msg.fields.size() >= 6) event.kwargs({}) = move(msg.as(5)); auto self = this->shared_from_this(); - const auto& localSubs = kv->second; for (const auto& subKv: localSubs) dispatchEvent(subKv.second, event); } @@ -573,8 +571,7 @@ class Client : public ClientInterface, public Dialogue { std::ostringstream oss; oss << "Received an EVENT that is not subscribed to " - "(with subId=" << event.subId() - << " pubId=" << event.pubId() << ")"; + "(with subId=" << subId << " pubId=" << pubId << ")"; warn(oss.str()); } } @@ -583,7 +580,7 @@ class Client : public ClientInterface, public Dialogue { auto self = this->shared_from_this(); const auto& slot = sub.slot; - this->post([this, self, slot, event]() + sub.iosvc->post([this, self, slot, event]() { // Copy the subscription and publication IDs before the Event // object gets moved away. @@ -619,7 +616,8 @@ class Client : public ClientInterface, public Dialogue if (kv != registry_.end()) { auto self = this->shared_from_this(); - Invocation inv({}, self, requestId, move(msg.as(3))); + Invocation inv({}, self, requestId, kv->second.iosvc, + move(msg.as(3))); if (msg.fields.size() >= 5) inv.args({}) = move(msg.as(4)); if (msg.fields.size() >= 6) @@ -640,7 +638,7 @@ class Client : public ClientInterface, public Dialogue { auto self = this->shared_from_this(); const auto& slot = reg.slot; - this->post([this, self, slot, invocation]() + reg.iosvc->post([this, self, slot, invocation]() { // Copy the request ID before the Invocation object gets moved away. auto reqId = invocation.requestId(); @@ -655,11 +653,11 @@ class Client : public ClientInterface, public Dialogue break; case Outcome::Type::result: - yield(reqId, std::move(outcome.result({}))); + yield(reqId, std::move(outcome).asResult()); break; case Outcome::Type::error: - yield(reqId, std::move(outcome.error({}))); + yield(reqId, std::move(outcome).asError()); break; default: @@ -680,16 +678,16 @@ class Client : public ClientInterface, public Dialogue } template - bool checkError(std::error_code ec, THandler& handler) + bool checkError(std::error_code ec, THandler&& handler) { if (ec) - this->post(handler, ec); + std::forward(handler)(ec); return !ec; } - template + template bool checkReply(WampMsgType type, std::error_code ec, const Message& reply, - SessionErrc defaultErrc, const AsyncHandler& handler) + SessionErrc defaultErrc, THandler&& handler) { bool success = checkError(ec, handler); if (success) @@ -707,8 +705,10 @@ class Client : public ClientInterface, public Dialogue if (reply.size() >= 7 && !reply.as(6).empty()) oss << ", ArgsKv=" << reply.at(6); - AsyncResult result(make_error_code(errc), oss.str()); - this->post(handler, result); + using ResultType = typename ResultTypeOfHandler< + typename std::decay::type >::Type; + ResultType result(make_error_code(errc), oss.str()); + std::forward(handler)(std::move(result)); } else assert((reply.type == type) && "Unexpected WAMP message type"); @@ -720,13 +720,13 @@ class Client : public ClientInterface, public Dialogue SessionErrc defaultErrc) { auto self = this->shared_from_this(); - checkReply(type, ec, reply, defaultErrc, + checkReply(type, ec, reply, defaultErrc, AsyncHandler( [this, self](AsyncResult result) { if (!result) warn(error::Failure::makeMessage(result.errorCode(), result.errorInfo())); - }); + })); } void warn(const std::string& log) @@ -766,7 +766,7 @@ class Client : public ClientInterface, public Dialogue TopicMap topics_; Readership readership_; Registry registry_; - LogHandler warningHandler_; + AsyncTask warningHandler_; Message publishMsg_; Message publishArgsMsg_; diff --git a/cppwamp/include/cppwamp/internal/clientinterface.hpp b/cppwamp/include/cppwamp/internal/clientinterface.hpp index 349ab6ef..43858040 100644 --- a/cppwamp/include/cppwamp/internal/clientinterface.hpp +++ b/cppwamp/include/cppwamp/internal/clientinterface.hpp @@ -11,14 +11,14 @@ #include #include #include -#include "../asyncresult.hpp" -#include "../dialoguedata.hpp" +#include "../peerdata.hpp" #include "../error.hpp" #include "../registration.hpp" #include "../sessiondata.hpp" #include "../subscription.hpp" #include "../variant.hpp" #include "../wampdefs.hpp" +#include "asynctask.hpp" #include "callee.hpp" #include "subscriber.hpp" @@ -41,7 +41,6 @@ class ClientInterface : public Callee, public Subscriber using WeakPtr = std::weak_ptr; using EventSlot = std::function; using CallSlot = std::function; - using LogHandler = std::function; static const Object& roles(); @@ -49,30 +48,28 @@ class ClientInterface : public Callee, public Subscriber virtual SessionState state() const = 0; - virtual void join(Realm&& realm, AsyncHandler handler) = 0; + virtual void join(Realm&& realm, AsyncTask&& hander) = 0; - virtual void leave(Reason&& reason, AsyncHandler&& handler) = 0; + virtual void leave(Reason&& reason, AsyncTask&& handler) = 0; virtual void disconnect() = 0; virtual void terminate() = 0; virtual void subscribe(Topic&& topic, EventSlot&& slot, - AsyncHandler handler) = 0; + AsyncTask&& handler) = 0; virtual void publish(Pub&& pub) = 0; - virtual void publish(Pub&& pub, AsyncHandler&& handler) = 0; + virtual void publish(Pub&& pub, AsyncTask&& handler) = 0; virtual void enroll(Procedure&& procedure, CallSlot&& slot, - AsyncHandler&& handler) = 0; + AsyncTask&& handler) = 0; - virtual void call(Rpc&& rpc, AsyncHandler&& handler) = 0; + virtual void call(Rpc&& rpc, AsyncTask&& handler) = 0; - virtual void setLogHandlers(LogHandler warningHandler, - LogHandler traceHandler) = 0; - - virtual void postpone(std::function functor) = 0; + virtual void setLogHandlers(AsyncTask warningHandler, + AsyncTask traceHandler) = 0; }; inline const Object& ClientInterface::roles() diff --git a/cppwamp/include/cppwamp/internal/corosession.ipp b/cppwamp/include/cppwamp/internal/corosession.ipp index 3b1e68dc..e1403560 100644 --- a/cppwamp/include/cppwamp/internal/corosession.ipp +++ b/cppwamp/include/cppwamp/internal/corosession.ipp @@ -13,11 +13,13 @@ namespace wamp //------------------------------------------------------------------------------ template typename CoroSession::Ptr CoroSession::create( + AsioService& userIosvc, /**< IO service used for executing + user handlers. */ const Connector::Ptr& connector /**< Connection details for the transport to use. */ ) { - return Ptr(new CoroSession(connector)); + return Ptr(new CoroSession(userIosvc, {connector})); } //------------------------------------------------------------------------------ @@ -25,11 +27,13 @@ typename CoroSession::Ptr CoroSession::create( //------------------------------------------------------------------------------ template typename CoroSession::Ptr CoroSession::create( + AsioService& userIosvc, /**< IO service used for executing + user handlers. */ const ConnectorList& connectors /**< A list of connection details for the transports to use. */ ) { - return Ptr(new CoroSession(connectors)); + return Ptr(new CoroSession(userIosvc, connectors)); } //------------------------------------------------------------------------------ @@ -245,32 +249,22 @@ Result CoroSession::call( //------------------------------------------------------------------------------ /** @details Has the same effect as - ~~~~~~~~~~~~~~~~~~ - iosvc.post(yield); - ~~~~~~~~~~~~~~~~~~ - where `iosvc` is the asynchronous I/O service used by the client's - underlying transport. - - @pre The client must have already established a transport connection. */ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + this->userIosvc().post(yield); + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */ //------------------------------------------------------------------------------ template template void CoroSession::suspend(YieldContext yield) { - CPPWAMP_LOGIC_CHECK(!!this->impl(), "Session is not connected"); - using boost::asio::handler_type; - using Handler = typename handler_type, void()>::type; - Handler handler(yield); - boost::asio::async_result result(handler); - this->postpone(handler); - return result.get(); + this->userIosvc().post(yield); } //------------------------------------------------------------------------------ template template TResult CoroSession::run(TYieldContext&& yield, std::error_code* ec, - TDelegate&& delegate) + TDelegate&& delegate) { using boost::asio::handler_type; using Handler = typename handler_type +#include "varianttraits.hpp" + +namespace wamp +{ + +namespace internal +{ + +//------------------------------------------------------------------------------ +template +struct UnpackedCoroArgGetter +{ + template + static NthTypeOf get(const Array& args) + { + using TargetType = NthTypeOf; + try + { + return args.at(N).to(); + } + catch(const error::Conversion&) + { + std::ostringstream oss; + oss << "Expected type " << ArgTraits::typeName() + << " for arg index " << N + << ", but got type " << typeNameOf(args.at(N)); + throw UnpackError(oss.str()); + } + } +}; + +} // namespace internal + + +//------------------------------------------------------------------------------ +template +CoroEventUnpacker::CoroEventUnpacker(Slot slot) + : slot_(std::make_shared(std::move(slot))) +{} + +template +void CoroEventUnpacker::operator()(Event&& event) +{ + if (event.args().size() < sizeof...(A)) + { + std::ostringstream oss; + oss << "Expected " << sizeof...(A) + << " args, but only got " << event.args().size(); + throw internal::UnpackError(oss.str()); + } + + // Use the integer parameter pack technique shown in + // http://stackoverflow.com/a/7858971/245265 + using Seq = typename internal::GenIntegerSequence::type; + invoke(std::move(event), Seq()); +} + +template +template +void CoroEventUnpacker::invoke(Event&& event, + internal::IntegerSequence) +{ + auto slot = slot_; + boost::asio::spawn(event.iosvc(), [slot, event](Yield yield) + { + Array args = event.args(); + using Getter = internal::UnpackedCoroArgGetter; + (*slot)(std::move(event), Getter::template get(args)..., yield); + }); +} + +template +CoroEventUnpacker, TArgs...> unpackedCoroEvent(TSlot&& slot) +{ + return CoroEventUnpacker, TArgs...>( + std::forward(slot)); +} + + +//------------------------------------------------------------------------------ +template +BasicCoroEventUnpacker::BasicCoroEventUnpacker(Slot slot) + : slot_(std::make_shared(std::move(slot))) +{} + +template +void BasicCoroEventUnpacker::operator()(Event&& event) +{ + if (event.args().size() < sizeof...(A)) + { + std::ostringstream oss; + oss << "Expected " << sizeof...(A) + << " args, but only got " << event.args().size(); + throw internal::UnpackError(oss.str()); + } + + // Use the integer parameter pack technique shown in + // http://stackoverflow.com/a/7858971/245265 + using Seq = typename internal::GenIntegerSequence::type; + invoke(std::move(event), Seq()); +} + +template +template +void BasicCoroEventUnpacker::invoke(Event&& event, + internal::IntegerSequence) +{ + auto slot = slot_; + Array args = std::move(event).args(); + + boost::asio::spawn(event.iosvc(), [slot, args](Yield yield) + { + using Getter = internal::UnpackedCoroArgGetter; + (*slot)(Getter::template get(args)..., yield); + }); +} + +template +BasicCoroEventUnpacker, TArgs...> +basicCoroEvent(TSlot&& slot) +{ + return BasicCoroEventUnpacker, TArgs...>( + std::forward(slot)); +} + + +//------------------------------------------------------------------------------ +template +CoroInvocationUnpacker::CoroInvocationUnpacker(Slot slot) + : slot_(std::make_shared(std::move(slot))) +{} + +template +Outcome CoroInvocationUnpacker::operator()(Invocation&& inv) +{ + if (inv.args().size() < sizeof...(A)) + { + std::ostringstream oss; + oss << "Expected " << sizeof...(A) + << " args, but only got " << inv.args().size(); + throw internal::UnpackError(oss.str()); + } + + // Use the integer parameter pack technique shown in + // http://stackoverflow.com/a/7858971/245265 + using Seq = typename internal::GenIntegerSequence::type; + invoke(std::move(inv), Seq()); + + return Outcome::deferred(); +} + +template +template +void CoroInvocationUnpacker::invoke(Invocation&& inv, + internal::IntegerSequence) +{ + auto slot = slot_; + boost::asio::spawn(inv.iosvc(), [slot, inv](Yield yield) + { + try + { + Array args = inv.args(); + using Getter = internal::UnpackedCoroArgGetter; + Outcome outcome = (*slot)(std::move(inv), + Getter::template get(args)..., + yield); + + switch (outcome.type()) + { + case Outcome::Type::deferred: + // Do nothing + break; + + case Outcome::Type::result: + inv.yield(std::move(outcome).asResult()); + break; + + case Outcome::Type::error: + inv.yield(std::move(outcome).asError()); + break; + + default: + assert(false && "Unexpected wamp::Outcome::Type enumerator"); + } + + } + catch (const Error& e) + { + inv.yield(e); + } + }); +} + +template +CoroInvocationUnpacker, TArgs...> +unpackedCoroRpc(TSlot&& slot) +{ + return CoroInvocationUnpacker, TArgs...>( + std::forward(slot) ); +} + +//------------------------------------------------------------------------------ +template +BasicCoroInvocationUnpacker::BasicCoroInvocationUnpacker(Slot slot) + : slot_(std::make_shared(std::move(slot))) +{} + +template +Outcome BasicCoroInvocationUnpacker::operator()(Invocation&& inv) +{ + if (inv.args().size() < sizeof...(A)) + { + std::ostringstream oss; + oss << "Expected " << sizeof...(A) + << " args, but only got " << inv.args().size(); + throw internal::UnpackError(oss.str()); + } + + // Use the integer parameter pack technique shown in + // http://stackoverflow.com/a/7858971/245265 + using Seq = typename internal::GenIntegerSequence::type; + using IsVoidResult = BoolConstant::value>; + invoke(IsVoidResult{}, std::move(inv), Seq()); + + return Outcome::deferred(); +} + +template +template +void BasicCoroInvocationUnpacker::invoke(TrueType, Invocation&& inv, + internal::IntegerSequence) +{ + auto slot = slot_; + boost::asio::spawn(inv.iosvc(), [slot, inv](Yield yield) + { + try + { + Array args = std::move(inv).args(); + using Getter = internal::UnpackedCoroArgGetter; + (*slot)(Getter::template get(args)..., yield); + inv.yield(); + } + catch (const Error& e) + { + inv.yield(e); + } + }); +} + +template +template +void BasicCoroInvocationUnpacker::invoke(FalseType, Invocation&& inv, + internal::IntegerSequence) +{ + auto slot = slot_; + boost::asio::spawn(inv.iosvc(), [slot, inv](Yield yield) + { + try + { + Array args = std::move(inv).args(); + using Getter = internal::UnpackedCoroArgGetter; + ResultType result = (*slot)(Getter::template get(args)..., + yield); + inv.yield(Result().withArgs(std::move(result))); + } + catch (const Error& e) + { + inv.yield(e); + } + }); +} + +template +BasicCoroInvocationUnpacker, TResult, TArgs...> +basicCoroRpc(TSlot&& slot) +{ + return BasicCoroInvocationUnpacker, TResult, TArgs...>( + std::forward(slot) ); +} + +} // namespace wamp diff --git a/cppwamp/include/cppwamp/internal/legacyasioendpoint.hpp b/cppwamp/include/cppwamp/internal/legacyasioendpoint.hpp deleted file mode 100644 index 9351f311..00000000 --- a/cppwamp/include/cppwamp/internal/legacyasioendpoint.hpp +++ /dev/null @@ -1,64 +0,0 @@ -/*------------------------------------------------------------------------------ - Copyright Butterfly Energy Systems 2014-2015. - Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at - http://www.boost.org/LICENSE_1_0.txt) -------------------------------------------------------------------------------*/ - -#ifndef CPPWAMP_INTERNAL_LEGACYASIOENDPOINT_HPP -#define CPPWAMP_INTERNAL_LEGACYASIOENDPOINT_HPP - -#include -#include "../asiodefs.hpp" -#include "../rawsockoptions.hpp" -#include "asioendpoint.hpp" -#include "legacyasiotransport.hpp" -#include "rawsockhandshake.hpp" - -namespace wamp -{ - -namespace internal -{ - -//------------------------------------------------------------------------------ -template -class LegacyAsioEndpoint : - public AsioEndpoint -{ -public: - using Establisher = TEstablisher; - - LegacyAsioEndpoint(Establisher&& est, int codecId, - RawsockMaxLength maxLength) - : Base(std::move(est)), - codecId_(codecId), - maxLength_(maxLength) - {} - -protected: - using Handshake = internal::RawsockHandshake; - - virtual void onEstablished() override - { - Base::complete(codecId_, - RawsockHandshake::byteLengthOf(maxLength_), - RawsockHandshake::byteLengthOf(maxLength_)); - } - - virtual void onHandshakeReceived(Handshake) override {} - - virtual void onHandshakeSent(Handshake) override {} - -private: - using Base = AsioEndpoint; - - int codecId_; - RawsockMaxLength maxLength_; -}; - -} // namespace internal - -} // namespace wamp - -#endif // CPPWAMP_INTERNAL_LEGACYASIOENDPOINT_HPP diff --git a/cppwamp/include/cppwamp/internal/legacyasiotransport.hpp b/cppwamp/include/cppwamp/internal/legacyasiotransport.hpp deleted file mode 100644 index 6d80f818..00000000 --- a/cppwamp/include/cppwamp/internal/legacyasiotransport.hpp +++ /dev/null @@ -1,84 +0,0 @@ -/*------------------------------------------------------------------------------ - Copyright Butterfly Energy Systems 2014-2015. - Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at - http://www.boost.org/LICENSE_1_0.txt) -------------------------------------------------------------------------------*/ - -#ifndef CPPWAMP_LEGACYASIOTRANSPORT_HPP -#define CPPWAMP_LEGACYASIOTRANSPORT_HPP - -#include "../error.hpp" -#include "asiotransport.hpp" -#include "endian.hpp" - -namespace wamp -{ - -namespace internal -{ - -//------------------------------------------------------------------------------ -template -class LegacyAsioTransport : public AsioTransport -{ -private: - using Base = AsioTransport; - -public: - using Ptr = std::shared_ptr; - using Socket = TSocket; - using SocketPtr = typename Base::SocketPtr; - using Buffer = typename Base::Buffer; - using PingHandler = typename Base::PingHandler; - - static Ptr create(SocketPtr&& socket, size_t maxTxLength, - size_t maxRxLength) - { - return Ptr(new LegacyAsioTransport(std::move(socket), maxTxLength, - maxRxLength)); - } - - void ping(Buffer, PingHandler) - { - CPPWAMP_LOGIC_ERROR("Ping messages are not supported " - "on LegacyAsioTransport"); - } - - using Base::post; - -protected: - LegacyAsioTransport(SocketPtr&& socket, size_t maxTxLength, - size_t maxRxLength) - : Base(std::move(socket), maxTxLength, maxRxLength) - {} - - void sendMessage(RawsockMsgType type, Buffer&& message) - { - assert(this->isOpen() && "Attempting to send on bad transport"); - assert((message->length() <= this->maxSendLength()) && - "Outgoing message is longer than allowed by transport"); - - message->header_ = endian::nativeToBig32(message->length()); - if (this->txQueue_.empty()) - transmit(std::move(message)); - else - this->txQueue_.push(std::move(message)); - } - - void processHeader() - { - size_t length = endian::bigToNative32(this->rxBuffer_->header_); - if ( this->check(length <= this->maxReceiveLength(), - TransportErrc::badRxLength) ) - { - this->receivePayload(RawsockMsgType::wamp, length); - } - } -}; - -} // namespace internal - -} // namespace wamp - -#endif // CPPWAMP_LEGACYASIOTRANSPORT_HPP diff --git a/cppwamp/include/cppwamp/internal/passkey.hpp b/cppwamp/include/cppwamp/internal/passkey.hpp index dbc769cc..266e6fce 100644 --- a/cppwamp/include/cppwamp/internal/passkey.hpp +++ b/cppwamp/include/cppwamp/internal/passkey.hpp @@ -19,7 +19,7 @@ namespace internal { PassKey() {} - template friend class Dialogue; + template friend class Peer; template friend class Client; friend class wamp::Session; }; diff --git a/cppwamp/include/cppwamp/internal/dialogue.hpp b/cppwamp/include/cppwamp/internal/peer.hpp similarity index 95% rename from cppwamp/include/cppwamp/internal/dialogue.hpp rename to cppwamp/include/cppwamp/internal/peer.hpp index 13a2ff32..8c2e070f 100644 --- a/cppwamp/include/cppwamp/internal/dialogue.hpp +++ b/cppwamp/include/cppwamp/internal/peer.hpp @@ -16,10 +16,11 @@ #include #include #include "../codec.hpp" -#include "../dialoguedata.hpp" +#include "../peerdata.hpp" #include "../error.hpp" #include "../variant.hpp" #include "../wampdefs.hpp" +#include "asynctask.hpp" #include "wampmessage.hpp" namespace wamp @@ -30,11 +31,10 @@ namespace internal //------------------------------------------------------------------------------ // Base class providing session functionality common to both clients and -// routers. This class is extended by Client to implement a client session. +// router peers. This class is extended by Client to implement a client session. //------------------------------------------------------------------------------ template -class Dialogue : - public std::enable_shared_from_this> +class Peer : public std::enable_shared_from_this> { public: using Codec = TCodec; @@ -50,7 +50,7 @@ class Dialogue : using RxHandler = std::function; using LogHandler = std::function; - Dialogue(TransportPtr&& transport) + Peer(TransportPtr&& transport) : transport_(std::move(transport)) { initMessages(); @@ -67,7 +67,7 @@ class Dialogue : if (!transport_->isStarted()) { - std::weak_ptr self(this->shared_from_this()); + std::weak_ptr self(this->shared_from_this()); transport_->start( [self](Buffer buf) @@ -169,7 +169,8 @@ class Dialogue : fail(make_error_code(errc)); } - void setTraceHandler(LogHandler handler) {traceHandler_ = handler;} + void setTraceHandler(AsyncTask handler) + {traceHandler_ = std::move(handler);} template void post(TFunctor&& fn) {transport_->post(std::forward(fn));} @@ -292,7 +293,7 @@ class Dialogue : if (state_ != State::shuttingDown) { auto self = this->shared_from_this(); - post(std::bind(&Dialogue::onInbound, self, std::move(msg))); + post(std::bind(&Peer::onInbound, self, std::move(msg))); } break; } @@ -315,7 +316,7 @@ class Dialogue : assert(state_ == State::establishing); state_ = State::established; auto self = this->shared_from_this(); - post(std::bind(&Dialogue::onInbound, self, std::move(msg))); + post(std::bind(&Peer::onInbound, self, std::move(msg))); } void processWelcome(Message&& msg) @@ -441,7 +442,7 @@ class Dialogue : } TransportPtr transport_; - LogHandler traceHandler_; + AsyncTask traceHandler_; State state_ = State::closed; RequestMap requestMap_; RequestId nextRequestId_ = 0; diff --git a/cppwamp/include/cppwamp/internal/dialoguedata.ipp b/cppwamp/include/cppwamp/internal/peerdata.ipp similarity index 100% rename from cppwamp/include/cppwamp/internal/dialoguedata.ipp rename to cppwamp/include/cppwamp/internal/peerdata.ipp diff --git a/cppwamp/include/cppwamp/internal/session.ipp b/cppwamp/include/cppwamp/internal/session.ipp index 4d959afd..9c4e2e24 100644 --- a/cppwamp/include/cppwamp/internal/session.ipp +++ b/cppwamp/include/cppwamp/internal/session.ipp @@ -6,6 +6,7 @@ ------------------------------------------------------------------------------*/ #include +#include #include "config.hpp" namespace wamp @@ -16,11 +17,13 @@ namespace wamp @return A shared pointer to the created session object. */ //------------------------------------------------------------------------------ CPPWAMP_INLINE Session::Ptr Session::create( + AsioService& userIosvc, /**< IO service in which to post all + user-provided handlers. */ const Connector::Ptr& connector /**< Connection details for the transport to use. */ ) { - return Ptr(new Session(connector)); + return Ptr(new Session(userIosvc, {connector})); } //------------------------------------------------------------------------------ @@ -30,11 +33,13 @@ CPPWAMP_INLINE Session::Ptr Session::create( @throws error::Logic if `connectors.empty() == true` */ //------------------------------------------------------------------------------ CPPWAMP_INLINE Session::Ptr Session::create( + AsioService& userIosvc, /**< IO service in which to post all + user-provided handlers. */ const ConnectorList& connectors /**< A list of connection details for the transports to use. */ ) { - return Ptr(new Session(connectors)); + return Ptr(new Session(userIosvc, connectors)); } //------------------------------------------------------------------------------ @@ -60,6 +65,12 @@ CPPWAMP_INLINE Session::~Session() reset(); } +//------------------------------------------------------------------------------ +CPPWAMP_INLINE AsioService& Session::userIosvc() const +{ + return userIosvc_; +} + //------------------------------------------------------------------------------ CPPWAMP_INLINE SessionState Session::state() const { @@ -82,7 +93,11 @@ CPPWAMP_INLINE void Session::setWarningHandler( LogHandler handler /**< Function called to handle warnings. */ ) { - warningHandler_ = std::move(handler); + warningHandler_ = AsyncTask + { + userIosvc_, + [handler](AsyncResult warning) {handler(warning.get());} + }; } //------------------------------------------------------------------------------ @@ -96,7 +111,11 @@ CPPWAMP_INLINE void Session::setTraceHandler( LogHandler handler /**< Function called to handle log traces. */ ) { - traceHandler_ = std::move(handler); + traceHandler_ = AsyncTask + { + userIosvc_, + [handler](AsyncResult trace) {handler(trace.get());} + }; } //------------------------------------------------------------------------------ @@ -127,7 +146,7 @@ CPPWAMP_INLINE void Session::connect( state_ = State::connecting; isTerminating_ = false; currentConnector_ = nullptr; - doConnect(0, handler); + doConnect(0, {userIosvc_, handler}); } //------------------------------------------------------------------------------ @@ -151,7 +170,7 @@ CPPWAMP_INLINE void Session::join( ) { CPPWAMP_LOGIC_CHECK(state() == State::closed, "Session is not closed"); - impl_->join(std::move(realm), std::move(handler)); + impl_->join(std::move(realm), {userIosvc_, std::move(handler)}); } //------------------------------------------------------------------------------ @@ -174,7 +193,7 @@ CPPWAMP_INLINE void Session::leave( { CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); - impl_->leave(std::move(reason), std::move(handler)); + impl_->leave(std::move(reason), {userIosvc_, std::move(handler)}); } //------------------------------------------------------------------------------ @@ -246,7 +265,7 @@ CPPWAMP_INLINE void Session::subscribe( CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); using std::move; - impl_->subscribe(move(topic), move(slot), move(handler)); + impl_->subscribe(move(topic), move(slot), {userIosvc_, move(handler)}); } //------------------------------------------------------------------------------ @@ -300,7 +319,7 @@ CPPWAMP_INLINE void Session::unsubscribe( CPPWAMP_LOGIC_CHECK(!!sub, "The subscription is empty"); CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); - impl_->unsubscribe(sub, std::move(handler)); + impl_->unsubscribe(sub, {userIosvc_, std::move(handler)}); } //------------------------------------------------------------------------------ @@ -335,7 +354,7 @@ CPPWAMP_INLINE void Session::publish( { CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); - impl_->publish(std::move(pub), std::move(handler)); + impl_->publish(std::move(pub), {userIosvc_, std::move(handler)}); } //------------------------------------------------------------------------------ @@ -353,13 +372,17 @@ CPPWAMP_INLINE void Session::publish( - Some other `std::error_code` for protocol and transport errors. @throws error::Logic if `this->state() != SessionState::established` */ //------------------------------------------------------------------------------ -CPPWAMP_INLINE void Session::enroll(Procedure procedure, CallSlot slot, - AsyncHandler handler) +CPPWAMP_INLINE void Session::enroll( + Procedure procedure, /**< The procedure to register. */ + CallSlot slot, /**< The handler to execute when the RPC is invoked. */ + AsyncHandler handler /**< Handler to invoke when + the enroll operation completes. */ +) { CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); using std::move; - impl_->enroll(move(procedure), move(slot), move(handler)); + impl_->enroll(move(procedure), move(slot), {userIosvc_, move(handler)}); } //------------------------------------------------------------------------------ @@ -410,7 +433,7 @@ CPPWAMP_INLINE void Session::unregister( CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); if (impl_) - impl_->unregister(reg, std::move(handler)); + impl_->unregister(reg, {userIosvc_, std::move(handler)}); } //------------------------------------------------------------------------------ @@ -435,24 +458,26 @@ CPPWAMP_INLINE void Session::call( { CPPWAMP_LOGIC_CHECK(state() == State::established, "Session is not established"); - impl_->call(std::move(rpc), std::move(handler)); + impl_->call(std::move(rpc), {userIosvc_, std::move(handler)}); } //------------------------------------------------------------------------------ -CPPWAMP_INLINE Session::Session(const Connector::Ptr& connector) - : connectors_({connector->clone()}) {} - -//------------------------------------------------------------------------------ -CPPWAMP_INLINE Session::Session(const ConnectorList& connectors) +CPPWAMP_INLINE Session::Session(AsioService& userIosvc, + const ConnectorList& connectors) + : userIosvc_(userIosvc) { CPPWAMP_LOGIC_CHECK(!connectors.empty(), "Connector list is empty"); for (const auto& cnct: connectors) connectors_.push_back(cnct->clone()); + + setWarningHandler( [](std::string warning) + { + std::cerr << "[CppWAMP] Warning: " << warning << "\n"; + }); } //------------------------------------------------------------------------------ -CPPWAMP_INLINE void Session::doConnect(size_t index, - AsyncHandler handler) +CPPWAMP_INLINE void Session::doConnect(size_t index, AsyncTask handler) { currentConnector_ = connectors_.at(index); std::weak_ptr self(shared_from_this()); @@ -479,7 +504,7 @@ CPPWAMP_INLINE void Session::doConnect(size_t index, ec = make_error_code( SessionErrc::allTransportsFailed); } - handler(ec); + std::move(handler)(ec); } } } @@ -489,7 +514,7 @@ CPPWAMP_INLINE void Session::doConnect(size_t index, state_ = State::closed; impl_ = impl; impl_->setLogHandlers(warningHandler_, traceHandler_); - handler(index); + std::move(handler)(index); } } }); @@ -499,9 +524,5 @@ CPPWAMP_INLINE void Session::doConnect(size_t index, CPPWAMP_INLINE std::shared_ptr Session::impl() {return impl_;} -//------------------------------------------------------------------------------ -CPPWAMP_INLINE void Session::postpone(std::function functor) - {impl_->postpone(functor);} - } // namespace wamp diff --git a/cppwamp/include/cppwamp/internal/sessiondata.ipp b/cppwamp/include/cppwamp/internal/sessiondata.ipp index 609f0f44..e6df7757 100644 --- a/cppwamp/include/cppwamp/internal/sessiondata.ipp +++ b/cppwamp/include/cppwamp/internal/sessiondata.ipp @@ -293,12 +293,23 @@ CPPWAMP_INLINE String&Pub::topic(internal::PassKey) {return topic_;} // Event //****************************************************************************** +/** @post `this->empty() == true` */ CPPWAMP_INLINE Event::Event() {} +CPPWAMP_INLINE bool Event::empty() const {return iosvc_ == nullptr;} + CPPWAMP_INLINE SubscriptionId Event::subId() const {return subId_;} CPPWAMP_INLINE PublicationId Event::pubId() const {return pubId_;} +/** @returns the same object as Session::userIosvc(). + @pre `this->empty() == false` */ +CPPWAMP_INLINE AsioService& Event::iosvc() const +{ + CPPWAMP_LOGIC_CHECK(!empty(), "Event is empty"); + return *iosvc_; +} + /** @details This function checks the value of the `EVENT.Details.publisher|integer` detail. See [Publisher Identification][pub_ident] in the advanced WAMP spec. @@ -336,10 +347,11 @@ CPPWAMP_INLINE Variant Event::topic() const } CPPWAMP_INLINE Event::Event(internal::PassKey, SubscriptionId subId, - PublicationId pubId, Object&& details) + PublicationId pubId, AsioService* iosvc, Object&& details) : Options(std::move(details)), subId_(subId), - pubId_(pubId) + pubId_(pubId), + iosvc_(iosvc) {} CPPWAMP_INLINE std::ostream& operator<<(std::ostream& out, const Event& event) @@ -539,6 +551,34 @@ CPPWAMP_INLINE Outcome::~Outcome() CPPWAMP_INLINE Outcome::Type Outcome::type() const {return type_;} +/** @pre this->type() == Type::result */ +CPPWAMP_INLINE const Result& Outcome::asResult() const & +{ + assert(type_ == Type::result); + return value_.result; +} + +/** @pre this->type() == Type::result */ +CPPWAMP_INLINE Result&& Outcome::asResult() && +{ + assert(type_ == Type::result); + return std::move(value_.result); +} + +/** @pre this->type() == Type::error */ +CPPWAMP_INLINE const Error& Outcome::asError() const & +{ + assert(type_ == Type::error); + return value_.error; +} + +/** @pre this->type() == Type::error */ +CPPWAMP_INLINE Error&& Outcome::asError() && +{ + assert(type_ == Type::error); + return std::move(value_.error); +} + /** @post `this->type() == other.type()` */ CPPWAMP_INLINE Outcome& Outcome::operator=(const Outcome& other) { @@ -653,25 +693,16 @@ CPPWAMP_INLINE void Outcome::destruct() } } -CPPWAMP_INLINE Result& Outcome::result(internal::PassKey) -{ - assert(type_ == Type::result); - return value_.result; -} - -CPPWAMP_INLINE Error& Outcome::error(internal::PassKey) -{ - assert(type_ == Type::error); - return value_.error; -} - //****************************************************************************** // Invocation //****************************************************************************** +/** @post `this->empty() == true` */ CPPWAMP_INLINE Invocation::Invocation() {} +CPPWAMP_INLINE bool Invocation::empty() const {return iosvc_ == nullptr;} + CPPWAMP_INLINE bool Invocation::calleeHasExpired() const { return callee_.expired(); @@ -679,6 +710,14 @@ CPPWAMP_INLINE bool Invocation::calleeHasExpired() const CPPWAMP_INLINE RequestId Invocation::requestId() const {return id_;} +/** @returns the same object as Session::userIosvc(). + @pre `this->empty() == false` */ +CPPWAMP_INLINE AsioService& Invocation::iosvc() const +{ + CPPWAMP_LOGIC_CHECK(!empty(), "Invocation is empty"); + return *iosvc_; +} + /** @details This function checks if the `INVOCATION.Details.receive_progress|bool` detail is `true`. See [Progressive Call Results][prog_calls] in the advanced @@ -744,10 +783,11 @@ CPPWAMP_INLINE void Invocation::yield(Error error) const } CPPWAMP_INLINE Invocation::Invocation(internal::PassKey, CalleePtr callee, - RequestId id, Object&& details) + RequestId id, AsioService* iosvc, Object&& details) : Options(std::move(details)), callee_(callee), - id_(id) + id_(id), + iosvc_(iosvc) {} CPPWAMP_INLINE std::ostream& operator<<(std::ostream& out, diff --git a/cppwamp/include/cppwamp/internal/subscriber.hpp b/cppwamp/include/cppwamp/internal/subscriber.hpp index 9c9de02f..5a9a5dec 100644 --- a/cppwamp/include/cppwamp/internal/subscriber.hpp +++ b/cppwamp/include/cppwamp/internal/subscriber.hpp @@ -10,8 +10,8 @@ #include #include -#include "../asyncresult.hpp" #include "../wampdefs.hpp" +#include "asynctask.hpp" namespace wamp { @@ -29,10 +29,10 @@ class Subscriber virtual ~Subscriber() {} - virtual void unsubscribe(const Subscription& handle) = 0; + virtual void unsubscribe(const Subscription& sub) = 0; - virtual void unsubscribe(const Subscription& handle, - AsyncHandler handler) = 0; + virtual void unsubscribe(const Subscription& sub, + AsyncTask&& handler) = 0; }; } // namespace internal diff --git a/cppwamp/include/cppwamp/internal/tcp.ipp b/cppwamp/include/cppwamp/internal/tcp.ipp index 7f21312a..0b7b8716 100644 --- a/cppwamp/include/cppwamp/internal/tcp.ipp +++ b/cppwamp/include/cppwamp/internal/tcp.ipp @@ -7,7 +7,6 @@ #include #include "asioconnector.hpp" -#include "legacyasioendpoint.hpp" #include "rawsockconnector.hpp" #include "tcpopener.hpp" @@ -23,14 +22,4 @@ Connector::Ptr connector(AsioService& iosvc, TcpHost host) return ConcreteConnector::create(iosvc, std::move(host)); } - -//------------------------------------------------------------------------------ -template -Connector::Ptr legacyConnector(AsioService& iosvc, TcpHost host) -{ - using Endpoint = internal::LegacyAsioEndpoint; - using ConcreteConnector = internal::RawsockConnector; - return ConcreteConnector::create(iosvc, std::move(host)); -} - } // namespace wamp diff --git a/cppwamp/include/cppwamp/internal/uds.ipp b/cppwamp/include/cppwamp/internal/uds.ipp index 16ea1da4..d426b251 100644 --- a/cppwamp/include/cppwamp/internal/uds.ipp +++ b/cppwamp/include/cppwamp/internal/uds.ipp @@ -7,7 +7,6 @@ #include #include "asioconnector.hpp" -#include "legacyasioendpoint.hpp" #include "rawsockconnector.hpp" #include "udsopener.hpp" @@ -23,14 +22,4 @@ Connector::Ptr connector(AsioService& iosvc, UdsPath path) return ConcreteConnector::create(iosvc, std::move(path)); } - -//------------------------------------------------------------------------------ -template -Connector::Ptr legacyConnector(AsioService& iosvc, UdsPath path) -{ - using Endpoint = internal::LegacyAsioEndpoint; - using ConcreteConnector = internal::RawsockConnector; - return ConcreteConnector::create(iosvc, std::move(path)); -} - } // namespace wamp diff --git a/cppwamp/include/cppwamp/internal/unpacker.ipp b/cppwamp/include/cppwamp/internal/unpacker.ipp index 8375b8af..36122fc0 100644 --- a/cppwamp/include/cppwamp/internal/unpacker.ipp +++ b/cppwamp/include/cppwamp/internal/unpacker.ipp @@ -107,9 +107,9 @@ void BasicEventUnpacker::operator()(Event&& event) template template void BasicEventUnpacker::invoke(Event&& event, - internal::IntegerSequence) + internal::IntegerSequence) { - Array args = event.args(); + Array args = std::move(event).args(); using Getter = internal::UnpackedArgGetter; slot_(Getter::template get(args)...); } @@ -162,6 +162,7 @@ InvocationUnpacker, TArgs...> unpackedRpc(TSlot&& slot) std::forward(slot) ); } + //------------------------------------------------------------------------------ template BasicInvocationUnpacker::BasicInvocationUnpacker(Slot slot) @@ -191,7 +192,7 @@ template Outcome BasicInvocationUnpacker::invoke(TrueType, Invocation&& inv, internal::IntegerSequence) { - Array args = inv.args(); + Array args = std::move(inv).args(); using Getter = internal::UnpackedArgGetter; slot_(Getter::template get(args)...); return {}; @@ -202,10 +203,10 @@ template Outcome BasicInvocationUnpacker::invoke(FalseType, Invocation&& inv, internal::IntegerSequence) { - Array args = inv.args(); + Array args = std::move(inv).args(); using Getter = internal::UnpackedArgGetter; ResultType result = slot_(Getter::template get(args)...); - return wamp::Result().withArgs(std::move(result)); + return Result().withArgs(std::move(result)); } template diff --git a/cppwamp/include/cppwamp/dialoguedata.hpp b/cppwamp/include/cppwamp/peerdata.hpp similarity index 94% rename from cppwamp/include/cppwamp/dialoguedata.hpp rename to cppwamp/include/cppwamp/peerdata.hpp index 0748dbe0..41167973 100644 --- a/cppwamp/include/cppwamp/dialoguedata.hpp +++ b/cppwamp/include/cppwamp/peerdata.hpp @@ -5,8 +5,8 @@ http://www.boost.org/LICENSE_1_0.txt) ------------------------------------------------------------------------------*/ -#ifndef CPPWAMP_DIALOGUEDATA_HPP -#define CPPWAMP_DIALOGUEDATA_HPP +#ifndef CPPWAMP_PEERDATA_HPP +#define CPPWAMP_PEERDATA_HPP #include "options.hpp" #include "payload.hpp" @@ -74,7 +74,7 @@ class Error : public Options, public Payload } // namespace wamp #ifndef CPPWAMP_COMPILED_LIB -#include "./internal/dialoguedata.ipp" +#include "./internal/peerdata.ipp" #endif -#endif // CPPWAMP_DIALOGUEDATA_HPP +#endif // CPPWAMP_PEERDATA_HPP diff --git a/cppwamp/include/cppwamp/session.hpp b/cppwamp/include/cppwamp/session.hpp index 3a21ac12..0e09897e 100644 --- a/cppwamp/include/cppwamp/session.hpp +++ b/cppwamp/include/cppwamp/session.hpp @@ -19,14 +19,16 @@ #include #include #include +#include "asiodefs.hpp" #include "asyncresult.hpp" -#include "dialoguedata.hpp" +#include "peerdata.hpp" #include "connector.hpp" #include "error.hpp" #include "registration.hpp" #include "sessiondata.hpp" #include "subscription.hpp" #include "wampdefs.hpp" +#include "internal/asynctask.hpp" #include "internal/clientinterface.hpp" namespace wamp @@ -54,6 +56,10 @@ namespace wamp listed under **Returns** refer to results that are returned via AsyncResult. + The `boost::asio::io_service` passed via `create()` is used when executing + handler functions passed-in by the user. This can be the same, or different + than the `io_service` passed to the `Connector` creation functions. + @par Aborting Asynchronous Operations All pending asynchronous operations can be _aborted_ by dropping the client connection via Session::disconnect. Pending post-join operations can be also @@ -93,10 +99,10 @@ class Session : public std::enable_shared_from_this using CallSlot = std::function; /** Creates a new Session instance. */ - static Ptr create(const Connector::Ptr& connector); + static Ptr create(AsioService& userIosvc, const Connector::Ptr& connector); /** Creates a new Session instance. */ - static Ptr create(const ConnectorList& connectors); + static Ptr create(AsioService& userIosvc, const ConnectorList& connectors); /** Obtains a dictionary of roles and features supported on the client side. */ @@ -113,6 +119,10 @@ class Session : public std::enable_shared_from_this /// @name Observers /// @{ + + /** Obtains the IO service used to execute user-provided handlers. */ + AsioService& userIosvc() const; + /** Returns the current state of the session. */ SessionState state() const; /// @} @@ -182,27 +192,23 @@ class Session : public std::enable_shared_from_this /// @} protected: - explicit Session(const Connector::Ptr& connector); + explicit Session(AsioService& userIosvc, const ConnectorList& connectors); - explicit Session(const ConnectorList& connectors); - - void doConnect(size_t index, AsyncHandler handler); + void doConnect(size_t index, AsyncTask handler); std::shared_ptr impl(); - void postpone(std::function functor); - private: + AsioService& userIosvc_; ConnectorList connectors_; Connector::Ptr currentConnector_; - LogHandler warningHandler_; - LogHandler traceHandler_; + AsyncTask warningHandler_; + AsyncTask traceHandler_; SessionState state_ = State::disconnected; bool isTerminating_ = false; std::shared_ptr impl_; }; - } // namespace wamp #ifndef CPPWAMP_COMPILED_LIB diff --git a/cppwamp/include/cppwamp/sessiondata.hpp b/cppwamp/include/cppwamp/sessiondata.hpp index 3449b258..57fa0e55 100644 --- a/cppwamp/include/cppwamp/sessiondata.hpp +++ b/cppwamp/include/cppwamp/sessiondata.hpp @@ -13,7 +13,8 @@ #include #include #include -#include "dialoguedata.hpp" +#include "asiodefs.hpp" +#include "peerdata.hpp" #include "options.hpp" #include "payload.hpp" #include "variant.hpp" @@ -185,12 +186,19 @@ class Event : public Options, public Payload /** Default constructor. */ Event(); + /** Returns `false` if the Event has been initialized and is ready + for use. */ + bool empty() const; + /** Obtains the subscription ID associated with this event. */ PublicationId subId() const; /** Obtains the publication ID associated with this event. */ PublicationId pubId() const; + /** Obtains the IO service used to execute user-provided handlers. */ + AsioService& iosvc() const; + /** Obtains an optional publisher ID integer. */ Variant publisher() const; @@ -203,12 +211,13 @@ class Event : public Options, public Payload private: SubscriptionId subId_ = -1; - PublicationId pubId_ = -1; + PublicationId pubId_ = -1; + AsioService* iosvc_ = nullptr; public: // Internal use only Event(internal::PassKey, SubscriptionId subId, PublicationId pubId, - Object&& details); + AsioService* iosvc, Object&& details); }; std::ostream& operator<<(std::ostream& out, const Event& event); @@ -370,6 +379,18 @@ class Outcome /** Obtains the object type being contained. */ Type type() const; + /** Accesses the stored Result object. */ + const Result& asResult() const &; + + /** Steals the stored Result object. */ + Result&& asResult() &&; + + /** Accesses the stored Error object. */ + const Error& asError() const &; + + /** Steals the stored Error object. */ + Error&& asError() &&; + /** Copy-assignment operator. */ Outcome& operator=(const Outcome& other); @@ -392,11 +413,6 @@ class Outcome Result result; Error error; } value_; - -public: - // Internal use only - Result& result(internal::PassKey); - Error& error(internal::PassKey); }; @@ -412,6 +428,10 @@ class Invocation : public Options, public Payload /** Default constructor */ Invocation(); + /** Returns `false` if the Invocation has been initialized and is ready + for use. */ + bool empty() const; + /** Determines if the Session object that dispatched this invocation still exists or has expired. */ bool calleeHasExpired() const; @@ -419,6 +439,9 @@ class Invocation : public Options, public Payload /** Returns the request ID associated with this RPC invocation. */ RequestId requestId() const; + /** Obtains the IO service used to execute user-provided handlers. */ + AsioService& iosvc() const; + /** Checks if the caller requested progressive results. */ bool isProgressive() const; @@ -442,11 +465,12 @@ class Invocation : public Options, public Payload // Internal use only using CalleePtr = std::weak_ptr; Invocation(internal::PassKey, CalleePtr callee, RequestId id, - Object&& details); + AsioService* iosvc, Object&& details); private: CalleePtr callee_; RequestId id_ = -1; + AsioService* iosvc_ = nullptr; }; std::ostream& operator<<(std::ostream& out, const Invocation& inv); diff --git a/cppwamp/include/cppwamp/tcp.hpp b/cppwamp/include/cppwamp/tcp.hpp index 40ff0bbb..2e9fbe0c 100644 --- a/cppwamp/include/cppwamp/tcp.hpp +++ b/cppwamp/include/cppwamp/tcp.hpp @@ -35,25 +35,6 @@ Connector::Ptr connector( TcpHost host ///< TCP host address and other socket options. ); - -//------------------------------------------------------------------------------ -/** Creates a Connector that can establish a TCP raw socket transport on - non-conformant routers. - This is an interim Connector for connecting to routers that do not yet - support handshaking on their raw socket transports. Handshaking was - introduced in [version e2c4e57][e2c4e57] of the advanced WAMP specification. - [e2c4e57]: https://github.com/tavendo/WAMP/commit/e2c4e5775d89fa6d991eb2e138e2f42ca2469fa8 - @relates TcpHost - @returns a `std::shared_ptr` to a Connector - @tparam TCodec The serialization to use over this transport. - @see Connector, Json, Msgpack, makeUds, makeLegacyTcp */ -//------------------------------------------------------------------------------ -template -Connector::Ptr legacyConnector( - AsioService& iosvc, ///< The I/O service to be used by the transport. - TcpHost host ///< TCP host address and other socket options. -); - } // namespace wamp #ifndef CPPWAMP_COMPILED_LIB diff --git a/cppwamp/include/cppwamp/tcphost.hpp b/cppwamp/include/cppwamp/tcphost.hpp index 309fd7a5..cfef6ccb 100644 --- a/cppwamp/include/cppwamp/tcphost.hpp +++ b/cppwamp/include/cppwamp/tcphost.hpp @@ -27,7 +27,7 @@ namespace wamp //------------------------------------------------------------------------------ /** Contains TCP host address information, as well as other socket options. - @see IpOptions, RawsockOptions, connector, legacyConnector */ + @see IpOptions, RawsockOptions, connector */ //------------------------------------------------------------------------------ class TcpHost : public IpOptions { diff --git a/cppwamp/include/cppwamp/types/set.hpp b/cppwamp/include/cppwamp/types/set.hpp new file mode 100644 index 00000000..6be0300c --- /dev/null +++ b/cppwamp/include/cppwamp/types/set.hpp @@ -0,0 +1,72 @@ +/*------------------------------------------------------------------------------ + Copyright Butterfly Energy Systems 2014-2015. + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at + http://www.boost.org/LICENSE_1_0.txt) +------------------------------------------------------------------------------*/ + +#ifndef CPPWAMP_TYPES_SET_HPP +#define CPPWAMP_TYPES_SET_HPP + +//------------------------------------------------------------------------------ +/** @file + Provides facilities allowing Variant to interoperate with std::set. */ +//------------------------------------------------------------------------------ + +#include +#include "../conversion.hpp" +#include "../error.hpp" + +namespace wamp +{ + +//------------------------------------------------------------------------------ +/** Performs the conversion from an array variant to a `std::set`. + Users should not use this function directly. Use Variant::to instead. */ +//------------------------------------------------------------------------------ +template +void convert(FromVariantConverter& conv, std::set& set) +{ + const auto& variant = conv.variant(); + if (!variant.is()) + { + throw error::Conversion("Attempting to convert non-array variant " + "to std::set"); + } + + std::set newSet; + const auto& array = variant.as(); + for (Array::size_type i=0; i()); + } + catch (const error::Conversion& e) + { + std::string msg = e.what(); + msg += " (for element #" + std::to_string(i) + ")"; + throw error::Conversion(msg); + } + } + set = std::move(newSet); +} + +//------------------------------------------------------------------------------ +/** Performs the conversion from a `std::set` to an array variant. + Users should not use this function directly. Use Variant::from instead. */ +//------------------------------------------------------------------------------ +template +void convert(ToVariantConverter& conv, std::set& set) +{ + Array array; + for (const auto& elem: set) + { + array.emplace_back(Variant::from(elem)); + } + conv.variant() = std::move(array); +} + +} // namespace wamp + +#endif // CPPWAMP_TYPES_SET_HPP diff --git a/cppwamp/include/cppwamp/types/unorderedmap.hpp b/cppwamp/include/cppwamp/types/unorderedmap.hpp index 52797f70..b2decd37 100644 --- a/cppwamp/include/cppwamp/types/unorderedmap.hpp +++ b/cppwamp/include/cppwamp/types/unorderedmap.hpp @@ -14,9 +14,10 @@ std::unordered_map. */ //------------------------------------------------------------------------------ -#include #include +#include #include "../conversion.hpp" +#include "../error.hpp" namespace wamp { diff --git a/cppwamp/include/cppwamp/types/unorderedset.hpp b/cppwamp/include/cppwamp/types/unorderedset.hpp new file mode 100644 index 00000000..fd4bcaeb --- /dev/null +++ b/cppwamp/include/cppwamp/types/unorderedset.hpp @@ -0,0 +1,73 @@ +/*------------------------------------------------------------------------------ + Copyright Butterfly Energy Systems 2014-2015. + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at + http://www.boost.org/LICENSE_1_0.txt) +------------------------------------------------------------------------------*/ + +#ifndef CPPWAMP_TYPES_UNORDEREDSET_HPP +#define CPPWAMP_TYPES_UNORDEREDSET_HPP + +//------------------------------------------------------------------------------ +/** @file + Provides facilities allowing Variant to interoperate + with std::unordered_set. */ +//------------------------------------------------------------------------------ + +#include +#include "../conversion.hpp" +#include "../error.hpp" + +namespace wamp +{ + +//------------------------------------------------------------------------------ +/** Performs the conversion from an array variant to a `std::unordered_set`. + Users should not use this function directly. Use Variant::to instead. */ +//------------------------------------------------------------------------------ +template +void convert(FromVariantConverter& conv, std::unordered_set& set) +{ + const auto& variant = conv.variant(); + if (!variant.is()) + { + throw error::Conversion("Attempting to convert non-array variant " + "to std::unordered_set"); + } + + std::unordered_set newSet; + const auto& array = variant.as(); + for (Array::size_type i=0; i()); + } + catch (const error::Conversion& e) + { + std::string msg = e.what(); + msg += " (for element #" + std::to_string(i) + ")"; + throw error::Conversion(msg); + } + } + set = std::move(newSet); +} + +//------------------------------------------------------------------------------ +/** Performs the conversion from a `std::unordered_set` to an array variant. + Users should not use this function directly. Use Variant::from instead. */ +//------------------------------------------------------------------------------ +template +void convert(ToVariantConverter& conv, std::unordered_set& set) +{ + Array array; + for (const auto& elem: set) + { + array.emplace_back(Variant::from(elem)); + } + conv.variant() = std::move(array); +} + +} // namespace wamp + +#endif // CPPWAMP_TYPES_UNORDEREDSET_HPP diff --git a/cppwamp/include/cppwamp/uds.hpp b/cppwamp/include/cppwamp/uds.hpp index 2f6f46d5..ec7b4721 100644 --- a/cppwamp/include/cppwamp/uds.hpp +++ b/cppwamp/include/cppwamp/uds.hpp @@ -35,25 +35,6 @@ Connector::Ptr connector( UdsPath host ///< Unix domain socket path and other socket options. ); - -//------------------------------------------------------------------------------ -/** Creates a Connector that can establish a Unix domain socket transport on - non-conformant routers. - This is an interim Connector for connecting to routers that do not yet - support handshaking on their raw socket transports. Handshaking was - introduced in [version e2c4e57][e2c4e57] of the advanced WAMP specification. - [e2c4e57]: https://github.com/tavendo/WAMP/commit/e2c4e5775d89fa6d991eb2e138e2f42ca2469fa8 - @relates UdsPath - @returns a `std::shared_ptr` to a Connector - @tparam TCodec The serialization to use over this transport. - @see Connector, Json, Msgpack */ -//------------------------------------------------------------------------------ -template -Connector::Ptr legacyConnector( - AsioService& iosvc, ///< The I/O service to be used by the transport. - UdsPath host ///< Unix domain socket path and other socket options. -); - } // namespace wamp #ifndef CPPWAMP_COMPILED_LIB diff --git a/cppwamp/include/cppwamp/udspath.hpp b/cppwamp/include/cppwamp/udspath.hpp index 064e2e33..1840248c 100644 --- a/cppwamp/include/cppwamp/udspath.hpp +++ b/cppwamp/include/cppwamp/udspath.hpp @@ -27,7 +27,7 @@ namespace wamp //------------------------------------------------------------------------------ /** Contains a Unix domain socket path, as well as other socket options. - @see RawsockOptions, connector, legacyConnector */ + @see RawsockOptions, connector */ //------------------------------------------------------------------------------ class UdsPath : public RawsockOptions diff --git a/cppwamp/include/cppwamp/unpacker.hpp b/cppwamp/include/cppwamp/unpacker.hpp index b9670d29..ce7c0d3f 100644 --- a/cppwamp/include/cppwamp/unpacker.hpp +++ b/cppwamp/include/cppwamp/unpacker.hpp @@ -49,7 +49,7 @@ using DecayedSlot = typename std::decay::type; The [wamp::unpackedEvent](@ref EventUnpacker::unpackedEvent) convenience function should be used to construct instances of EventUnpacker. @see [wamp::unpackedEvent](@ref EventUnpacker::unpackedEvent) - @see @ref UnpackedCallSlots + @see @ref UnpackedEventSlots @tparam TSlot Function type to be wrapped. @tparam TArgs List of static types the event slot expects following the Event parameter. */ @@ -99,7 +99,7 @@ EventUnpacker, TArgs...> unpackedEvent(TSlot&& slot); This class differs from EventUnpacker in that the slot type is not expected to take an Event as the first parameter. @see [wamp::basicEvent](@ref BasicEventUnpacker::basicEvent) - @see @ref UnpackedCallSlots + @see @ref UnpackedEventSlots @tparam TSlot Function type to be wrapped. @tparam TArgs List of static types the event slot expects as arguments. */ //------------------------------------------------------------------------------ @@ -193,7 +193,7 @@ InvocationUnpacker, TArgs...> unpackedRpc(TSlot&& slot); The [wamp::basicRpc](@ref BasicInvocationUnpacker::basicRpc) convenience function should be used to construct instances of InvocationUnpacker. This class differs from InvocationUnpacker in that the slot type returns - void and is not expected to take an Invocation as the first parameter. + `TResult` and is not expected to take an Invocation as the first parameter. @see [wamp::basicRpc](@ref BasicInvocationUnpacker::basicRpc) @see @ref UnpackedCallSlots @tparam TSlot Function type to be wrapped. diff --git a/cppwamp/include/cppwamp/version.hpp b/cppwamp/include/cppwamp/version.hpp index c26bb5a9..6fc17b56 100644 --- a/cppwamp/include/cppwamp/version.hpp +++ b/cppwamp/include/cppwamp/version.hpp @@ -17,13 +17,13 @@ #define CPPWAMP_MAJOR_VERSION 0 /// Minor version with functionality added in a backwards-compatible manner. -#define CPPWAMP_MINOR_VERSION 5 +#define CPPWAMP_MINOR_VERSION 6 /// Patch version for backwards-compatible bug fixes. -#define CPPWAMP_PATCH_VERSION 3 +#define CPPWAMP_PATCH_VERSION 6 /// Integer version number, computed as `(major*10000) + (minor*100) + patch` -#define CPPWAMP_VERSION 503 +#define CPPWAMP_VERSION 600 namespace wamp { diff --git a/cppwamp/src/cppwamp.cpp b/cppwamp/src/cppwamp.cpp index 97a192f4..4d6927c1 100644 --- a/cppwamp/src/cppwamp.cpp +++ b/cppwamp/src/cppwamp.cpp @@ -10,7 +10,7 @@ #ifdef CPPWAMP_COMPILED_LIB #include -#include +#include #include #include #include @@ -30,7 +30,7 @@ #endif #include -#include +#include #include #include #include @@ -53,8 +53,6 @@ namespace wamp template Connector::Ptr connector(AsioService& iosvc, TcpHost host); template Connector::Ptr connector(AsioService& iosvc, TcpHost host); -template Connector::Ptr legacyConnector(AsioService& iosvc, TcpHost host); -template Connector::Ptr legacyConnector(AsioService& iosvc, TcpHost host); namespace internal { @@ -66,8 +64,6 @@ namespace internal #if CPPWAMP_HAS_UNIX_DOMAIN_SOCKETS template Connector::Ptr connector(AsioService& iosvc, UdsPath path); template Connector::Ptr connector(AsioService& iosvc, UdsPath path); -template Connector::Ptr legacyConnector(AsioService& iosvc, UdsPath path); -template Connector::Ptr legacyConnector(AsioService& iosvc, UdsPath path); namespace internal { diff --git a/doc/architecture.vpp b/doc/architecture.vpp index ca644b50bcdeb0993aeeaf2577ef216d391e0911..30dc152b410f8eac1fd12c33ecf88d1eb93cd614 100644 GIT binary patch delta 54642 zcma&N1ymft(=UpY|8BadMIkf-9MQI^ zn7*Aff1a8Rs`7(T{*F(BFaNP{>d9YTErG5}MRb?`biC8bGk1H#d8`0_KU_XOe-T0? zO9d`e(q!^s(#koTlD=VDWZZ&5m)~GOnf%>FgGSf*=czxxwmBI8xd%DAwvazGmqnM6 zmsj>TiEbOM1uII^Rv{DD65OFHu-9+X*o5@ea|eyX6lp=uom!tmE6VjJ_k1%FywA1| zqLL(g9(8)zM{L>td;T2*b1qwpaGK?xZo8AfDvNX^esU^52Tap?S?GH;PS{-XNPal2 z=-#OO-#-bynV;&AWjAO(+bFI(apai94rP22L}eZ5Jy-qvd4>|rSVq&Z8)&zsHBGF# z`cB6Pnk658S5_VfdlW3C9oR8S(0RYWbR%s{`S8I0AoM1YJNr4p zKt~{pJe6(i&i{Y`@(kZ(-A7;dXpK&W)CdFH2$z^hEkT#a8Ty0*0|PhX7!>J|C>bWI ziSg7Ym^cj$uS6G@Q)$z|6)6;>(i~{K!7C{lBl}zRF0sc4dBgETZ56i@! z*515cKF!$(kr}Uac?AgZic=VOv;g-Uf&!E)IV&;FCFfI=PQ_aXbNf%{CVl~KcMEUC zUy64qtFy@|yF^}Y*BBy@`FoGz_3a;StK!eFjYVEZZD-uhw~i8r?)MLWU0x0?cJJ8u zeGwEDnpIzX|L7|6aCCI<{A}$f)*&LK)WFzvIWuLdo&vB911$|kB3)f%t3d3ivs?Fs zvC8-A2>!k5GkQvA>~RHCFJD2iHbA(OKMNE0ZUD*eMiqnh2~Y85DC{-#o|c=nOcL$2 zHYgleOaOc~x<>uf)3pA=Dh~G{MMEMH75obNjxVB~8a91nr6S`H&~ zF(okZ7whg=4r$FaqQ9*P+P35(luMCEP;e=rUH7ismFN9dmQx!vi)uNn^`RB2Xc&UQ z;b7QsFkB-T9)T7}@&1%R7WJ0CC6Az)P$mh-LX&*Jq=_6T{B-)Fj3ey81IwD5fKJaU7Za)8;gvnr&MuQJub>VtZv27rpWMcY<=>*KWx9-3#T=)c$ z809Dxc%SB9YnFN3^!JJd!JKv+`E4F|A{yCe$Jac%U=tiMY&mw}O;g?)*Jc#Zhe5Pp zR8WrrVSyFTKwE4&_g4}v63(+7te-k9u{tfkv;mt38>F>3O8WVxy&(qM_@g^34-bB# z!x^sojrBLzuum`k`IRMh`!7*qJ2U%M4&UXN(rb&e+PpsdmhgZzh_rE6;<=RS-zR(D zdXTJP^%v+`i)G(NDAd{TKP$x;E?g8e4y}xr0@W`~w_Ta{aWjCQfS9PL@UxK({=-aj z7qERA<9s&vyqX5DSAMeg*qh_;P6ubDYa7Yf5%CT-vRvIZ+w!kS;v4$v>OYz}&JiLF zZ)w-0g?~iou%7&GJ}J*C|FYH4aH&_3_h63Sl>0NF{3oRY;!ANm9A3%^3iaV3*oOT? zrlCRlW?JP}^TpDVw{B2@CXiOu^~zY)6%3qb?I(kw|2g_3_d05y%ocZjYPzg_2`_tD z&3V0Izp&5m%9*e#x_+~|>D78l`}unwr4H52l*&hKVZlEhbN`zs5TXenH@>JS9I-FP z7%S|HEtO5M)A`yIi=8!XS{cn7rX8MURkZ<0O&v5#k?NevlgI5@5B0-9|39H6o&OQ~ zRTB_jdU#wwdU$7lRaNWLK)S}ipyDWIi`8V?op?mEonLc&tU2RuGfclk;kD+O&U% z*2f^-?zGEs>>2o&eX$ZLhr{HtrsPQS%hSboc9FZiSW8Q!^h3jqu)h7oh&}6Cq!?;z zgw>%2W9Pr)AG-{1UDsz#SBA#jmyRbqw5X|SmOBNWQdHdo}-%mUZ%XI@L1G&lNo&o27! zu{NN!CQYqn(ui8#=)>#+eqE^lyShXV@nunY<4^n&=EifA+Cwn$e=R=6fW}TL{5?Ze zf4u!%?{sQ&64G|oD@7amaS;Wk=@(9(mA$)Hu?cGAOS)N#$=3$0w44(ZTo@4^xJsIY zgXF@f(Exlk_t_N+VOzm7-Q#?B;tA=#Nk>`WeT8AcoJ#4lOzAOkirQ#sC;x*zDyqNp zSKXe@7C(k(kBaJOHHBxs+VlPLdTHN-R-bHBauhF{KI}>G$S4`9?uh|cf|`+(uF;1i zh)l~<=k;&-M@M%JD|*e3*D|I4?s}prZP!2Y#>6Btl!?8_gLLU~?o_1+w1m@=%?X)- z%Yycwwp@Ai5dJW|ilGso?!KwwH;l+fG%-=BCpp_0KZNu{L_-^`A|yK(vwQi`kl>=q zuRz2R{*GzhwciN5`k<3H`vGMav4c^UGi1j1P{6(MG@8=kkK_sm78Vnyh%$DM*HEXlX#jFpQ$q2lX9uKq28QRGReVpF$>mB&$`~Yc^nJMZ3_mkW2 z((KWzi#xf#yo*DV8jMljyDa`_PvEV~kQ(+)DUdQyloQ9R*mZQzN51&$@#t!6$zR(@ zs%A(ZKiC*EV zlj;*mz`9mfeOk7pK>1%4u?55<;Is6J%SqF#Tccw{EyWE?MlxZ|? z`rEU9;SHLb7NkR09)mVgqZX23Li}qJLa$RQ`{PdetqzB}7!ZAdH6{B`a+B?TW3RaW zrV0)Y)Ey;jkbrznZ6|;W+T#;awa?Z15N~sFF zqNUt^fwR(p8gA=HfE^HL&A#^G^UU>nF}z+7QpR62-}aq&1Dl2Q26UJ1|Cq`RY(b=L zA}p&xX^j74D^EVpG}gnzW58ZF-PFRP!8RbEl)*w~V37zRy|5_s*c<} za6(MsQF7p&EnFdzweX}65E>>b8ahe83tuZBN?9GGCBLyS=|u^}8D z$jFd0Crpwii68Kda1ziP|9!Ba^cPAOP`ZZF9n?pugHk8d9fpGvJe2;q##T_Wfl@P+ zenF`pA~}GFpZE>&7V^;(8NErV622U+aTFd70S@M_$s7eU3m($-9Ukkyz)+I_J5kpMek+GYis4(5(6W=0}L8xRfr5o4bk)n~#U|`PR=AllxJmixDg3>?H zyc&B}*pP{Sq_>b!2Lx&~*rU1+AViH_h5s%@M*tIh?m}wL(eWUs4hT4~sws{LB0vyr zXng2u+?=_#o-JLKR@?MpP#+BkQC}}jASRJH2!))UYnC8_DK`IQx$BFnT!sI{v7JX# z+hKL~LE3$r^=`&#+CkTAHjkyD`eA1MS1r$Byu_Sz!AQh2)PRHJ^uF#k2}_MNOR%^H zsTmf4So8ixZu)Z;oAbU1GAhGM1fWH>OYlC}1S9B$AZYE7^r5@zEiU>G7&BO4y3;gz)@NfBEg(8p+`iTBI_TM3kg$ zIr*QLI+hA!!d%yh-e(6~mhWg+lDKT+Bmc+ePcNL4C{f{ZN@mk&l$us%3bCpp zIq>_&U}+YT35l%w=A~iQsOvb#_$I_4MIQ6-|7gQ-o#gi{3(;|ll$hVCe}D(&#Q>>g zu_~LD52qGeO7zo1W@9W1gZcZ3-HAk$-11otWMeYIg8JM?ye?W-W$W@7nd?W8#nywT zpwS;jaI4Mk166e*BfGb?Y**8jUwzgM^>3GaYK{0N%>Dmx>3e4#2Q)JETJwlRJjceU zG$)RoS+%b-NAcDfyp76NodmG9(73m7v=`Scp0=5-?X5cG#=nY53P;zC%4}MM#9tIv zjHXz0QgJ(kn3Gutm{Vst1XypuY3s5M9VAqK^^mBGb{>h2(zm?yB^vNdscL6zb8l}Y zS*l;->1l^hE!VNt)5O4JLq#G=(8xe4QzJ;AiyKy0sVDmKw*F&K+@fPy0jK1)lTiqoivS=lHt~qod0$!{-RQ$z!7#zGA%^sr<|%?yS9d+bIhl ztQLnG`;>MYFlK|jxm_;9uj&GG7!zj@iA#eKP(YV*b;fzvDL+xQZRXl5j+Bkj`b^iEq1A*BFSnT&_O~@u5u<8%Oa`!=HmW(>2=0pK*p|Z)u<~dH&#DfsPXk+<$U+0cl&gyzAu}OK|VgKR<_$?KlsAb z&)qWH-Gx`IF%tn%2N6yj22Q+=S^^2S33QFX4vSpCOo8b5%MFrt3)QlQjv8A6xGw=+ zGz8jX5fwjmoCC^&7R`c8%joLde;$voLaHP%?$~Hs`bNi3tHsMK83IsgsXe}Bu?gj> zY-@SVI=MGl9IQv2#f8i_nQ&pg@9pJVY+9}|@e)3a=zZ+HfgGv&8a+nDy$T4R<{Js5 z28c9&-5%Ij_w(YnyAA;kPH4&Ae?EYi;>(5Pt~rH0_cr?a_?%q4W*t+KlT$uNO?ue> z_QjZP$nWY;$Y~U(XjdId!hG_XC%+Eq9te5aZ@dPD-eOOgW}x2Lt@W%=^GrGUIDEj)N}`0vNS-&3y4yj<*G)bJnYD9Zt=Tl0=RSN+%^gy`AJzePJ(Tp-wCZ1fkllS@#SZwx3$zmi7%&+RHu^>VM#YJm4E-Q4 zV=ODEv=kz#jLZ)?yGMwEPz596KnVVz{KH4# z-!wiUAW0y7f`R)4V+xh=smW9XF%R}1bov585<^r)uEW4bbleeupo$?vhlH*xX;W=L zS6A?_vfG*(8R{P*{dJ}2^9gYIV4ZQaICEiWJ^KlmVr^?7vRhsAMB=Ze2+xtL zItt#8KJ{7H;_s-zCB(UV{}$=hi&pDt>uU(J6Y;Ne_Q>g&SGuppF31+)3qSX7Vj?-ue>E1v@dQVR9GAYvC8ML*ENkqXZ&63a~0J z5ODhtRUrL+w&vo~YFbyQWbdr{$UMd>FP3ZqtT+2JsB1t(ncK_I;Q zOUvdB;qv3b%J%J)Nb6ABw)^IWPzCDi7RGG%{_fMk;`z-!hoDF^$y#*w3m@@vWJPbt z(S@7a?UkLw3p>5vd5CJ1H)Z(57SPx@Tv|jNt%@{Q*usR)VqNDF$18d`1%3^~`!#ZR zw|wgyyVfZv=6`u|RM49?t73iD^>pye9uW7^v~-X%N#}-YDAn|4zY+JCxA*aIq&gPx zdJGWfpvRd|R*memdzaC+e_2xNLm2vu)?n^p*Wg^Th-%D+Igu=py-^z&IvCI}v@WJnpqd zO=+WGnR&MhcjmXd=U!#?!a8Ku1b@TGK9mkG#o2_WCS3Bun_6-5+bH@+Rt(~9mW_+h zFS+?iA6*ep?MI7^s2;fpMR`YK?uYfl%%|g$514TbiN4j|bmqv; zb-_`M4|e=!!idYsV8K{;&83MZt^V7cHQIEUB?IqU@}RveeSw8PaD%T*;|o5HoVWa+ zQg#1Js^UIwtA)RO28##`aATL$L8*=a4LIb_DD#bSO zl!eG>*E7noRZmNLw>ASnv?ZgGk9&_4MfW#y_st)%$xH)-&Wr_ zq$nF&L@lWvT?8phSE(Sg&ibP&z8b@FGHeTx>Wj^Sb|anz zv>PY-eonX0Gih3?`B7^m%U&Sn!Ar<#FAbB=N06QF3#@ra-odKkEuzrWk@kG}r0Wtc#Fh18+jJWR2(^icCR4!4E~x zZ<5>gAX9gC%_qLI@VC}%5*kggE|22w`kjpk8JtM*q*Tn4gM$82v}>gU@qXsOp`Pr} zLOOoKTr*|m4jbv#f*}h+0zr!SYF=SW>Rb$y!EiyMxN!_i22F z>jNU0)6&R~rz^|o5O)5AoyS|>$o0P=$dBveS!}pl54{mcr>Vz;MT_YuJN=lLQ}XWX zUkVC@n!W2{_B(x>GDbq!D=MylExY`8+L|)RD>)RZkb57_lbaJPl{MyUF(yfwYKB)5 z6B2@UBRA6QcdveQ=u;&*^AZ{w%uL&P(qWT-G9OCIxVNRW7gQ#X>IR2mqDxPL_j?c# z-P|k^%+G&UXlHz&Nc}!#@y$K@Bnahp#;z$uswS-RUGmUJjk+|0eMLwT@cB@auH;RV zwq~DR=>Gmx0;fbi7MgHzmVlbdcs!7Jp#19+1UJ7eG-m$-L035u z9GaPv-d@(=jJjlZWOAqRA6otNzpt)?=2_gxZ+p|f&Zp?$P3a{_(&B*oyck@NXewc? z$Ngd|PPD#f%G&kcUzU8^Eia_~Ivd+;tT`)%Ywn|>!lwC?%Px> z_m=Ak_{$GlRXidsP0Cu@F0OSI(-4qI?r_gpW6S8BU^Pi>6O5&?>dtu~5Nf(n|cj6rk3?96^Ov=G0WKiV?Y+ERPG4B`2fix3zqx zF7Tt(p*)l@EX8wI1giX=T#$ya1uACMXtNVmPbj_29Mv?+O0oMJR?Lmp|1jw!&hwJhjHV#avF@crz;_k zw`X|W=j+|@HsZc9yw)`PeVl>f&np?{achraEQ9uc*}%+h{(UwI1yeugDzxP*HJMRm zd^#v@>aTtmMRa0BQJWnmB#gHRm#^>GUQ#)&+n)u0;QmZliFiH4-cKGPzUxszU`|^` z>uHc)M^lW*R(4>}(CCE-rv$+I2qx%b#WyUu$2Icvg7r=XT4G*hQu8Vet(uM*S7Djj zG;-Tp{FK@onqg?>-kT~rt~DpxowB*#8-FtXcIVUJkL{ycgzW z!PKX8Fw^RHkI|N+gM{-aOlg&mmjcpD+=G2%v_aH{xPJUh>icLH^+)@>Ak+gg*}%IC8P8##CHE^5a465a zk#Zk-Z0~P=fVlm*UxT3ZrMif>wX*g_*RXKBFxe^bn{@V&4^qM5rxlv;ph}VL`q-A` z3VK@O`Am74AM|MyuX(u}a^pl@QSsFaojTT~BzaQWJAA0$ZR%{%%@yD4s5eV{7c(D? zN!3dgdwz%$y|*f}W6fTc^Ulz(zRpfz&e%GV3Hnf(1H{}6Kagb2NogHg))iFnFgPZ@KVyzlB6Q<+8?UHZ{<=Jqu<&?XP@{x=AXu$j|+ITDP9^lv?=sKg63_GI5c)7L1Jz; z1Bdffk_M`0o4;nj-MDLASjq)2ULL0g?|p!K3i{8a-x+9Gc1M|mT`C6de@C(=^`5*w zBQ+8(^rWXY{K3R|EAC6(vz5Y{q4GC(FZSMH8E8y!7-%PCd*YVg+q1OetEKiJs07Og zS39rJ@O^r^#CXTD_qQk8t+n0f`uNL2`fB3fAR>t9E_U&+3Gt6g`}t8${eM-wtR6KT zk+c6q=d;qoARE2w;An{)I(wVQ(5vpc>kweZXWjT6wTm9DdKr1DAb6*|(-~5D^Il{s;yJ!dZ#f1o=6Mh)oM!IdE!>IAL4Znp(S*BUOPv^Ov>-N)t75~Tcht5;a(@$X$ z6S|P>PJmvO&E3UruSiN#O<{UwVV7%^KiXiaN`KFDYM2ql1|J_CsXqM^rPi8xgQy<) zFP18z)Yzud@IGw8iMC^wAOeE zV!!pKx9}fhOv>{%l^9GE@i5GKrb~rKAvf$8n*SM*4FA|1)fgkJ;U8_l<4^XWbUwcqI< zAEquc$HgT!{H8H0wOK^>Yvbjm_;)!-uN$N{Xim8@w&vT2oW$E)N4t;Uz<)oKVXwl_ zfwV$=?wd*E{W@!LhVN94$V&*G-x}}NyQfkte`C!rV&+OqS;-U@hL%Ll9n1{8%zZmq zZg5Qn2fl<8i?3$ED31P?rp#TaLO0=aNPVATs)R?#7a{29gQ3XGpVfdVd-ZDzbq`K=>D%LV_0F;eLb3uVuQeQGs%aw-(dgD2TSzJPGR^^%JCWpr;{|ykzJRI)!_K0%GT{FxC+Jc zDsLy@#%h%^ESZ4sGiC~$jVkw9oSCG_k6U{QNe-Pqyk4o^06s!};qno)tqLOB3=MamEa#sH|yb~EneeP%vM@&g_Xi-zg zb?t?M6;=PiN?>8g$zK5YuZ)A2>SZVE*{-;{qFq2GN#v;0 z_fbfr#BP1ya`jG#qSMz`wDa)KuaM=odwIyr2jiu)kh?hNuawWOpGCWb4-|-tbDrF$ zuVF&jAi&i_bR<0Rp9^#x1t{3lsmrn6kv7A^+`m74gebrMW?YgLLCEnfpb7>JY z@zJgGUWJ!)-hkBdoYKZnh7E>usXNr{dsN5^!l6>0U8KlnNu*b3ainI_zFg$~Q6x|9 z=vxuxp0`}eW-+wdbg*+=D=O>p9Ln+UpJ~gUhOad5S_-4WT2bkkF!OBHMTcg9zWEP+ zpUgg1X<;#oFwfw7@hEA0R72xefHjDl7bfu@RmHm?QpVGLJM}4Y->yuHs@UmWHvI-? z8hwpiLPbKLT?1UrPjl0Bnz%=K^?QYBjj*BwG5*GYPiUg^I{BC8cJ%IDGt z!UinU;o>4}(dIvFl;cY@=mmLg1E}K4eRw>|h2Lzbar_Av9;j0pywXD%EYfAD<%LlN zWN$lAL~wj){ubuYCQlTVR1+df`R6Bq8Oj*a7`)SUsJ{qv3dr`d&T-_*!CH>@@*sxc zFEIsb@PN7g!mXi$-%RqvOXYjeJm8nrpr^k1%nmZ5nxRFLUy}dHMA9IE38>hSeQlpq zO&H#rp3#ni%M(+R??W$p!^OUBYZlgb(zlKuqsQ3((I=0D?7x}YnQqJ4dC?fj%*dcc zE=BHZNX6*&wS#@*&uP-=)jmwCz^P1?$_Bj&ODn7aUs0XvzeflusUA>+rZK^V|7SM9 z=E6X~FfdJ+e-Lkw0pFVyPa5mi03sO-nuS@JmMS!x6Q(a2m1VQPGub4MMO-y`t*G^w zW}>pSzu1TUa*g_K?|#(K$Qu@Bb0(8bMS2@R?X(d2;Z(1W4bmoCU(Fr2&yKhFK2GPM za94PcIPNnB4N|;G^}iF&(JyNd=cNu(e1!D2hMX-14XPBt<2sUk7)W#Sqhzo?^*BR) zmcgUt4{M!<;_LF1IpH>+VfuF71~%^8ZG?O_xl#7EbdcnB`ngC5Sdx~feVED&go>#u z?7zKjZIpVU%HGLioxc+~q&j8+QnvA0Ie1(2H}ZA&@V5-oRA@K@qGWy98CM@=n{qh* z0L49NOIb6Z0(pxe-6w^Oc$>FX^;y`kcwpwa^xWN4DQ;`8mnuQVOCKZ?R74JPUl)?O zF%E$)8-~V=*8&KoVuJa-+-G2@Y|hTqadY9&^oGQ2LU7B4m2Y&7R**@3+2bAU^W(gT& zD&Xn#6s!(sY{^5d)l2e_H}j5Pc|=OK_g@tu6z>7i;QDgQ2xtxv^g7M!5^wg6n)@T< z3wr{s9~_*_@P#~9ru!IIv2eKK8erlN$mkeUQ}9eaH`g5WF$yrD5pP?Yn&CS~fYSzQ zccaq=TH$C8V-Y)S&ofvYdiHsgn`6hROVJ4p2JTr(qey0$T8L!CkZ`gv5Hpw?gkqZQ z5`3({-Pk;idr|N!+26fUd5l21zbz_Fe|fx z%>}o?KVlJ(@`};oy{}F4eZt$_aK=P!aXCRh>J9r=hdxp8RwkpD?4^SRr=-f48Rp*s z;8`lEw!%+LLmY0d>@12ZHKgD<0`iXqQ_>BM$h*bKEq&C!`;DxH2IzP*qbVbJR4$PK z4I0jx{k!J^VUa%~PMsg0So$qH&m>y057$dBL^IT}%jT&Rggo%_xi(wG$G!Yy`mGY! zwl8_5{PBBsM8EkTBSmW5W<@a0!O}DX(YC}9-=h}^tBC^}#s-Ei${0F>??!dL8{EE5 zz#PrsQk$C0vdF~x1QEEf(tX0a=xE#&Pukyy`FkI|s(6uu?xR|it*BL*C8wB!ws*SlBvXrg2hk2+=T~}^cHAzO zzCm5u8Pku)&UBB-x3)vAlM~u$PB|AtltK0j--&a!ANxxlf-Y=3-3_lc0 z=f0GpQizD4c(0KEkh`u~-&Fzw$hy`U+*5vh3eUet*7&uy7X+RJ?GD~Abzb&z2nnHe zbaeD1ZxNQx+1c3GP)QtI`y@D3(bqN^WS^^S&!^kaP`lg!(M1#5m64gK@m9I@T5LJb zLW=5q(khA_`gAQu2e@FCIgk#XsVg53kBnVT1U6*?5hA={@fe9ym+vib+MGV9)uK-F z$*C)aQPQ$C@>%GT{!&Mgeg5Aw;lZ#$(>6T)>;;Ml6h-Q&5m@AK$qKSCJtx}LM!cQ3 zWTrr6TFmv^vVCE!=a3vJ*QV~fWOsk1=mSPfa^C?*bX&pk_5mMZnPHU$H}1-Y&GxM8 z%{nZd`KU9+j=S#RUcd_Y@VDqg%?urgm7BYPkbvb4x56b&jTYVprMcaO%~Fv>8%bta zq=+Gr43DzqaZ)xPELX%i~8r>CCz#2b+y&9r1%V@%8mwl7x}fcCMmaPpD4x7aUvNb4;Q8wWx_T%KC2?(wHOmi7O{}Q2$erG zuX`L|BAHdxsWbR_=X8n=*SPAziWf;{Vb)QrCKv(>NF!@Ci?CVU?ttKOZ9e z3Fjv((LG;dd;2&xR!PC%5d3<>T&q;#A_3*_CucWk0HFTde0H2yk1V}?nxiQo z4sNl8nyB!F5E^$_-`N~R$pG$mxpxgeG1a~&y?GU2U6}Ls3KqHfjW;9ON^iD8f%o=> zm}}gAC@$b;(*X%5WYF|d`^hGqFi}~;4AcUXf35sJ&pHRzDSDOW;-0-T6h1Lpdq;$Nh#F(Q+SgT$*=|5 zeEP&6)RPGTRcR^0a1mi_Q5`-UlY|UHsQv*s4ta36_*q*kyC+@8Nf96|yn7;w+^x4!yg1jm!OE#+o}kv)2Hb0qSp_}j(o`ivOJ~9dOMT*y!Nz067d!xN8mbS% zW)RzS_#EZKX7QR>=4GH&xdsKpc7zV@xNK4*NcLDcd3mLJq11fBOSY>JlztVHh7N}W z3zD41ghrAnx@IZA-n(1wimFaLUG#eT&4|_)JPaC6D)m;I%mp9eh<(N@fFl3Mynt{JXJ{oGWhnfZQkQ%5Ux&?%cgpeX2i^}F64|lt?B5ffQct;EPurOa-+nq(Alz2+z49^<70!pJan+3!RQ|y;e zRN;eCN!VW~^kg9Mhk=a^l)`d9LIf-}uZ^C|ps?6B35Nes1B3Q2_jb0(zDDWdZQOa{ zYk=7dK1#fQPd}76{sP7tP{J+Rz4QkAon$e#Kf3DZzT(^$ndsqhLT4FP7t}BwCn;oMxOK?p zM|Bv}s)sX5&!N4tbayHXW%PWcIL20ise=M%O|?L;2Z zIl(fYt4Yg+;l;g9jo05ekAGgY@WG&AzkPo<-^ywt;-3l)%ZlUGsr_dlPn5!jIi7E0 z2cUGNAD+!7A^daW?w{ ztjxf)k*C%wmg-pYepyV={?L$k=I-ddvTjahFJlc3A>^QI!&gyyKx=k}_(piDli^Tw zY&K-+>JD3x;SPVb@eR%41?%blf_ZySnvYMfy~Kp8&!6u!a*^bqi;ki&cjLgp7yE5z z_I~ug8AHGN+}-M|4<(i1K*}x%G$ITLkuShl|K+)2b?DBV|F+-iT2bhEcGR{3F;xB1 zZV#j&Q2DIjfLUVy+%?jZcm`n5EN^5DR!$IZ^Jaa~Dt1zO-R>Aor_dhJr$!YH^~>_# zyI|0kzkljWI(y!~U!?ei*P=&A&>-rfm#($MvMnB8l@9h+xKs!Kh@i;gH+4YFF9H0? zX&^odmu}#GFPJ)vsgZFBeDxJ9PItR03)?gmk_CAORom6cOtl)gsmpPRq`6A`xn^-0 zsTn{4H-D#Ag>iDwkiKkKh#K^4zykhE#6!-j7$LmH1&~Kbaf34hZHko6rm2O{gv0ln z;b^Yd1K+yZ{#kycj+{H>;j(+vjmAmjGW2a99{KN3BLWf}1^@#i6njTbR!N4(4VDQ+ z$uW5d{X5V#!|;20=Ew{jV4bv3D)MrNXZj8wIZ(k&H%~e@!?o|H9Stlf%{SQIo*$0^ z3&O@^dbUbdOX(lXIre8}Bm$+4^s^TimWBr?A98Yd*ff=yR}i_}BrH%(;Wt75RE!C^ z@FS9Oau|0I1-cpI^?QG^cfi3Zx`Fu5c@c(g6V*uez2VL43$jVlR$m;eq<40ljmrTx zB$=2P{k&SP6faRhkIE#0@cqLjd65iS2KI>TY_eKbCR^g|?NBbLipBBSkPsJXj#j-q znBi)p#?`@7#)YTJ%T^_lp^*q>GwA8rO)L=*5`rlPlI&Xn2 zLBy%_LoDz|8O)*#)8QoO5Jycoh8o%d2XDx|%zqqcj#4bYj0qJ*xmY%V= za84B^>4h(5#F>S0lG~wguxi@9KQss`9*M~U^pNhr#F0`>W-d~k@x=MLIYgh1?7&dG z5rg4L}FbT+q#H@Tz*|X0U8RnE-%{ z8a@lqKkbK+e*v26Mzo0xBt&hb>Uoj1GvngQ-*u?@A^kW^K)_jja5llvlan;CMJ4q} zJd+ki76cxb>T4Rw^AL8Mr}5c1&u>AedBNPioi`WC!T}qLxi-pPJ(wI*CQ89bJ)=zK z@D~TYdJvz-YzP!I|9vm_<+o%^BG8{ZByVp|V*M4DLX2{E%s1+{#n4Wq%szs!P3`6v zP+k>g7AM%*-ke>=CM2W|l!q0H;Cl}bzqaAvCMESW7?79eIy9?t8Vm;DYYZ;;RH^qO z_M+}J9`i#(e-S!HeHOPf-d0lH#v3CR`yw!xkUB=Z#BP4F9XXc9yG(PV0`#p${*K}e z8LXrFs%~S_q(}Xsr(CZP8HYsM(NPl>S7^j738hsl$=(nNM$k zdw_xiF!Hi0GT)_4g8OVXkf3?GFfitg_(;$^VR)AR-&9~SBzQ1v#Q%31aVZiU3IIF4 z;%|b`qt)cL_i~9`Ba{yB?g>j&T$XN392C11L<-BQK3|PZr2Q%ReC_vdD)YV8(l|<0 zq1pg{lY(RqW~-yBPr=#E?5$+egRSnci^a#{R-;GDxeZg8I1RfZ-0sZ6;}8{GYsN(lkbl#r%D+IcY}g!cezA_E=ox z=c^8SeEM`zNP!WsJ8D#$oq}TX99Zm8Z0UW^r%s4(Ge~CrLv*d5msdD#1c2Ng)$_uU z7-Chp2wsInOGsS8D4T!Q)N%q2zaTpyU7_VZtzAu_Ec-v6d@u2G@auh>iwV#38OC|_ zsWyxhdrm?~Emj>OAuT?K{`bMtecX<~vFD0T^&SD<8NwXM-*nxUMri??;MN4)cSQ-XKN$A zePC^CyO}Z#QLIBf2CYSDr5~OWo`>5!L!gZ7gPFI}nr+xb=RG_G!+2#?D<@=lcvnBZ zJ5897wu>lcbN)`5qpQS{##dm})jj&V-m740dPR*;pUg@|Hks`eh0oz&W@hFR5b$y_ z0%4~NuPrYx&+qK)#7J@e)2#MKZ)Gt4?FwkJf290V1wFC(V|{%+QD{x<`?Vl=9{LhCsLb zZ}vu-o==vW(t?BG=ouIy0w2#edJB!9J)vt%9i0Q#`z;M{%Y6;Ine=lx04WBHzy|X5 zpE^`$2XO?@usu;SsAjWp6Hb~B#FASF1%~50lM5^y@+zKuC>=44Ua5%)(ED>Q&iw9O zoVqxc!ugsBF@i$*ECk6D0m1Ji5|1HP2GZLci%|IE$7kjSQFl?3w1>&r5~LQ-Goz@L zI5I)?K#qBUdfnxtxfCiA8#6Oa*Wwg^Vp`hJ?CdOPD;V%RS>mnzUREZr26@XhZCo(* z#H0s7LRMXyTJkY0H|Zp*66I;|my<)P;ryL>5;Vm1Ta=wW&W)x-HHY5avS}Y8+d~@-(8!Nvj$yTl>g7|5(Pa7gb**d z|NHF{jm=1KAUN30CUcp88y0?`U}7bjAfiG@9WX!q%WeGrf3uYW;~y3Z2fGIY2Z7y# z#s7^&6AZ%t+Cjm(j=|S>hxCF?3|Ef{_Xq6p6T`zNuPNpV zR%fd#?W2~DrKOC{hI|p0HwdcsZT!#;CcY9bIVxNz7dS18H1~|F zwp$`g!(!soE0|CBo0YiL&g&fa!}%;XS~9sT>10Tia<(12kMVQ^mF3>_#H`e_L`r?A z>+`)1b;Jf)Z*MbzE~naZ!44Pk@rg4Ag!*r4m{(lGVYB^a$R50z?6V8=r}M+*yD*rq zN|%^5U(~1n8VYo;HNrfTRcM{LV31q|H^s$SJE@F^kD%DRL*)5pm=_U(OEKYx#C7HM z0K0HgXr3>pPbmM7z+0+O|HHvwX_VS7fT2nd|HPjExMh-AN3BZ z&u(r{=)S2i*-9IK zTON={jWZ0L)K_Pnk8H##8R_Yp`W3FOuB5@ceJXwV(e~1rQV2XGO=;JT4n*+i=KgPU zrd7LN4#r%V4$Rc57L{UyVkhw@+0d#v-tl**joVA(JX<&I0uK| zt^pF$H!EpxQhl|hS2=_6coOed;bNO=cz>l%A5az?Gl9KDTyu9^>g@uJx z!yAZ!UsF%j8$a;n~y6=wVlm{I7$R*GWE~Tt9KQl=K1=xN{w(Cj zj~@)>v?#tG6nJ1Imwv=n&i`1p0EcC^3r5EFq zGEf#*8Z)9gO-B9ABO_%i+aKmu=PfcYz#=ZPh@N{vs}XVx0*<<^btbW>^@j5c@GZ`5&CC0H;lQ?mA9fdG ze9lfz2!8vC_|H+JlhT-+@D1xQJ2Y#!$H8LQyZZf?Ym}DOp-e}amacp>FaBLtf-*S{>JNk-_Q7J zqNpdVgI#I}-wV7k%p!a<`$?vUrZ^^==wdI@)05av&_~P9eXUbyqtNC} z)U+;1?iGbb;X_GkcD5fLE#9d)&ufb8B5tTNy6sV4cV~;;@?@*JVU=GwPRoHQjA?l2 z0}jsKmi0-t;AI8S3!(}>1Nql#d63q9DO@+(PC+H^s< z@Vta&eMF1Ip$EM~CD}WItkLA7Jge-?mC)G?RrvKCS;x)kHz#``-_hV|VJgutDHVx* z;Wg1=+@&sPFd`Th{;*12doDX}Fp)GX#By3-`H}b{ijqHYAmI_uI zRD(oX-c2cZ!;jXrRSFHBpOx4uXIgBjxE1CWmfW*~nG4kz~lbTO*C3&}g8P z2G^w*Z7O_?DkU?8O8rH$&jMjaR--1EbW^4PA$HA2vcH)_iS}j4wEZ%Xmx96a<>4?m zm|k6v1jqL{?X4mf`N+t!$PwM9@=~=C86c8CK~CpDO^~I}sPvGkQdF$EM}!p^5-wI= zUOsLPR!$x^syE2KrnZ(grs~X%#~NM}*^m=&6ubrm5yU5acvv?mShogbEyOt(M1ZM~ z*`DD+FtichK{#{~#X+4P2!QY>{wB|lBn4iokMw+=6ZeN=;c~1OkfGgQ=H=M%U~U0S zBQ$JAz9ncTB!0Kcdh+u}cm@5*B;kd0fEolE(H5}TfPG!BhX`!>prF#?FcMEWIH+~~ zdfz{fn6hH5Gh3L8j~xCfK^2ky^%f#61h5v7mwEDze7)7KSmS2UwK|0vBO_xnLjBB8 zBjiy2iKB>BaW@rUmUuy!TkXZ)hmtuK_YxNi2Xgx*Ss}1gA2*z82XjSPS=EcQ6?8ij(eTrCa`P>-)84L>owXU@= z_IC3TugEz_PD%Cx@$baRE{uEX&i1KMpT%J$i}-va^-)7`CT0Ym##BmwBS|Q0Jl41d zA>R-tbOS;W4$tS!v{y)pi+kFFbY_^5Rfm}4Mc>5Koxds>vcDW0_amwDeVFP9K`Abm zlLpDZC<8=%UdzGD1=>#(xflD%3{5jI7O9wUQv%X>x9O;d;sjyD0f+ysX#w|IaxfBJF|=o!)qg# zHj8+7sBU$v)hQj5=y&>;D6M8uNzKYn(ZsMHsHlXbv))`~8TMuz{Iq(z!~ZQkb^`uO zQMik!2!k^!MsGTm@h`U{dXBj`t-$<}laJ&DCUyoIU=iMGHZ-9OJP&Ypq{In7d-E(MT=jiB|oe+Pj>}O4cjjfh%PpmY^M{Psy;ctA2 z>x^ofGt&q1bp6rdZqFj8&r+t7Vvxm& zQLeIhon{po`SkR3*6`@4oq*fH+zqfuqg0m{9{m`};OQV_HSFZ8O{_mznbqj7#PCt+HlrexR(Qj#;xvhU?99bR zeOedOB=?6}`X9WjoUjmI0M3M23VThok1N|jM;|(4$ zc*Mg;b5;dIpV`xYR7XyMTL@h3z>exey#*qmMutHJyji$j<*BDqIP3=?y)d5qupthI z%cdkM@hD*(=Pg;$yJ{#vxdoFV#@eXK2`x*xZsBMUXYsx|osMQ*3)zUN3p>SnH+8?c z6StB-)GFJ5I-eR0`0h}F!|y-Ng#6o!=D)r>H_o@Gvv)Nur?%iTGhc@z#m*)~Wct-4 zXk;vHOjrpGPxtHOYvL%nN zfFC!v&0M&%mMY9CwVHq3M7J1I?cVoiU%TiBosLEK0njYQRs zOEyx}8~4}Y8DI8i?Ba4Br5s`igCku5TMo9-`NLPKoT^eBt`oQRf4pp7H2aFX@BEAa zu|4~MX`1=P96z=5dGj*g78x%;@H6+ZdPfnR4>VD3W&k6uEXDx?_! zL+gy zZ!9t?Rf&F&7eh_1K}#ISY>x;VNU!j*WM=3Elp!gIpjYw-g5?sAm^auJC{q7wXD0k_ zJHL1CCQ8<8l^ymJJ3mXMn{RN?i3Q`ZRSUW-SSUt0k6ENVQ+=ymr?eHE8toFmmn6zdCE}whJ3aGmn0CwycJ#J zd;|BxR6O@X-nXy2G#ICa?VCd$&Bk5zJBCG+-BF?x2m!WbK)^%{%O)TyXjgl7>=&Zo#R2rmT3ZYmbpE26 z%Oab?)_uj3J{ym;efX?*I$`ZiKWTF|wNcUK&ex^yun9M<1ss9nU1Er3wz&Mi+8Yh> zg0A?Rp~CG{Cu|3Jlbx#vhtW}|RhPY{80$;kmq#14g{8;j7V+RmfpJ5if{6!` zLVP?3)J=?uy2Iu7s4C#_-{r<#c8gNw)7%Gdmo`JP2&mxi62{RYFx&k{{fk_b#{VWJYN=%Z`M_w=ORn!GchXT%N~>cqUFGyL3e#gY30ChPw)&v>|YSz z|AJ7uq{dXaoJqFqRf;#|49BtDG-x^k&TlW0;O^Mki{3S?lXcA4e;&NVqsV^-0Oh{} zfM|Vt{kXnOn_Zb&R;W2)49)a?>i017Z>TcEAbi{ty=-T6?9IdSR{=42uN^j){{RsE z4*)Lz1%M>6@QUu&7uLD-Z$EHJMc>*p=YxH+RR<$|R+2>NP?8%?nSqgS-sonWSO2&# zPZg6&P6$H+*JWg#d&ndEvw4Bq>M{>AcnjXHeukruu?xrfLk*4c9R0`EYULM0wn-A) zORp?l4Z7~+aP+-@R_n!LA>ZlUpzSw98ZfA_mM!k8VWI71+p4rbPlV4J_lB&YEF&uf ze~W7L2Xv;e5YaENcnzLws9~^>SxXQuMBWw%RYPxx`UVMj4KlSgwR3?$yCJebPC#gI z5Hb@aP(y(s>U&toLOCqne;>ZBM+L!RLYpPBeZ*;SH$nOc3z4!0VM1^$K#KJgpl5-B zzz{K2VPEW7j{qyjx6!alm?*HI~?f;h(3is^m>g`Qd zwnG3NECS*&=OJ#AR6PBNQ5fFi6nY%fw(ycDRR7=6%sH+96%H6K}Yhn|9;?TQtG}tbz|L4}Gk4t@uCFZZ9 z+1H2ZmbqDoj(m22_@LEkw`c0Dx1>B4&fMnwc`nl5of+2~3w$zKnCp#Y zp=FVuZcOlZpMv-9Bg%^3z8{B){o!ww9Vd86a_YwY z5?ES$uQ4~k*cGdi|7&gS-?{b^xcRveh9S?x9AFKGsT5Qa+RAUJi(N#+(v*eVKIVq2&9$kYq=1Ggy*xfDCC0#b@V0>sd7G+LQ)mf!&2=5jpJr<$wk z7f<5%inaPA&wj$-zmCFx{Dc9~x$ZhgUGqVD?%Ja5ZeY0)hpLtWIsE(YBTU9;JPn&N z%cMCMFxlv&59cryrCls4sf@9!)hMxxSfy+vXk z2Lv*~a~z#$66rb}x6?Mq^|#Z;N?q^tQA)f+VUzd!v1wkbz?O&bkFRt+D+rbmO#JLJ zJCB3klpE7(W>#4FWe)e7-%hstBav)-TR%@^HCW1|(>STd z>g@u&CFP&VlWh|wBCy6plEWc;`kTeO>KI*g>Fo7o6KRofnzH10|4x28K85xH*w>nM z>`nq&15AiCW1#PdTumLA*kDh|P3>A{)_?hL{}-5{nq#Sg7PD7K`lj^?a}{<;3}HZxePk~X z=fy!vN%9vJm#8r^*mw8nr*9^_EdLA@_^)go`Il#GyRSqu_xDedC;%~K-3w}lmOXoj zUg<(zk)>suY+#F#6KhNKUyO(LG@=5s-XA)w)pJQ^f+WK$Tk7B=V+R5|8X9#)J_< zzwXoEh-;k#br!bMp#(uTYvfO=4~yZ3i_N%4gub{y6oKA&6nYdhG3x7Rh9Z2E_eQP2R&|EatY$CK=zTqbC2|)*d-|sLSYp2r>se}--MvIx&WdED>P_WL z9e)o_TjdkXSc(~4CUwJj^L?g47<8doUi*-FEeIX6DYpr7z@evikoZntH(tf6^t*}g>u+R;UHuuAKp1m3MJ9s7>0W+vh;avutM85p1rzaAkf89&$?v`wD4)$yJN zCjiwC#dwF&Mz`9YM!eYh8vWMLUx2B-Zf9psP}HOTQ((y2wDldye&ZIez1@}nw;CNn zd+P!If6sP4i_P5lpucQZHc!|7Z~PEl-Tt$lk;9XNtBLnL2+Z*NyE$56^@boorwAiS zrnrZdkv+8`MR4KNUslr1G}c`=ICI#VTgbmy-yul-ceX)`LQ6H65Eb~a(OG0~py%iS zGQ_$!P8h-jf7e$r#c}okJI@Lr(8ULzO9G{z?%}H+XJr4q$u{^;0FaKe5#Pyv#{Kae ze{l@(61pPKa_Ysd78Sc&n44*zX@s}rr{~Rm*Jgel{f{pcnPW&HNL|3HBPF?qpXQt*Ekzh4ph zeF)yPYbN}zhy5J~P6e~k8m)hWgSmM|rKiieT3S88)?paA90|}K7_V#hWh`4*YPJh? zafIVy*EzkeR@G-X)4=dq;XGedWqq|);2$gYLad=`_b3!aGggx5sG_DS8@r2~c<-i4 zK%ar(sP*@Cpe9)e`KOjPAfB>4D;b8_gb5glESS#GOe~U*GN@1u2tg9Jp5lPStF4P< zBc$mJ!+gtB>KPdBq`d_2Qc9!{noUmZX5Y8&JWI{;+qoyw`Zv2b#uf+0{M-)t1dEw}+S*r#=LdvMnq zz8W0M*8)AmxGo8X1Le$yb=QHNzG2CMBFHeW4$sC9ed&ROMlN zpb4@gpRWd-iFNUBpNSFuLPE#MR`U4z+zgR8(;bx=n1PNfNggPc+k|cHR`ipVn}ewKfej4Nj4=f9Qem7}a{IXO%S5mt~4_C9)r+re=rWetyRATS=u9d+?H*JG+g82&NTXrBY&kv^MjJbOn|@hcg9ur@L`rSL{ZGO)Eb z0ggS!8<-YA(|lkUDPH8Xtc5K`XE?IaZX0W*g|yIl#Lb@?=Iq}q2+WX%d3hny#oHW| zgSAGbqHKAN$>GhNk%cr$BiPjoqpzJeL_pWF_#dfRNxyg7IO^#v)HIQ=^;Jxn+cYpO4shWk1}(iD$_GKxKjtzX@ZN@gGCV5IJrVJ~p*cS6gN zuQ~pm6NMIUnHa37M&X+>s-(&!?M)e_GAcu6EgZxFX>EMuhd*+!n?@QwbDP38LSTqf zt0kCXu+-(-nWOUCc=mYF0_i*xT;6xXGIwHFyvAILx%3H1o{4Re3OFCd8tGM}=^}#Z zC-5UWu>io{#((-weB!-!{N?9QQA|T5@3=X5!O$gB-BR{)wpX)=UV$E)UvIMX7S+(A z328eq(ASnX92b65d`dtFDEAYtXpf7TmZAiwPVaaY4zjP;Ipwtg`n|zD7UE)K#WYmw z3vYe}T{1Y_nY~Ghxanhu{%~~-@(>3N^0C(g!U+=7-wKb!UMf9B3wsyGX zd#)f$^y9gF!8AT0Ns>-nIAacy=p&Q!o2Rr@R{`BE4TD4)h zgGbaXon<){8H2gUiW35z@}MEglNS&G0W}!3#XTW1zSz9zfmUMBa@@aCi*7BSVCYT7 z$2pqkI5hdJzvkI&Wj5pjd%-diFp!XOXoyUP=^{@pep$E_!O5_EOeuOU>BAsBa8hSOVD4jYf`s3zZJUoJ#rZOw>k8zCK9g~Jmlk=jIdpk*2}yXt65dq{$wwaxlk2eq2s#$d@v)qTrKu!&roG{ z!DsMu0D+=r?@xL8(zVB0y$9)yf)7Wqg?^;`f*$N$-(cYY)|5@$)};6N9y4VqtX9ggEeTCboY{jMQHrXd;YNW2#2S1_|hz&=MVptewbrV1}u0agu% zMTwN3KRB~SgGJ0U?Cp1TnuUCYJE^JL!zA^(DQk$tRnltGFnh!CA@N47D7v|VqGdmq z<7eT^>ubKXT2P#%)EZMb%(C}Pe_X-rQv5o<>ID@k9wdQREFPK2-fDsDLJXgVkip)a zSm&S&)F%+DXZ~(dtB+6P=!AfU3z~BA?n}f63N=

A=URDSLqpWB$;yVE$ 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 aodcHy&;wrt00000w_~6Tp97bce+XSmsd``%jb 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 diff --git a/doc/images/client_api.svg b/doc/images/client_api.svg index 34c752f2..40318e04 100644 --- a/doc/images/client_api.svg +++ b/doc/images/client_api.svg @@ -69,7 +69,7 @@ >DialogueDataPeerDataDialogueDataPeerDataT0..11..*0..1state_10..1DialoguePeerDialogueDataPeerDataDialogueDataPeerDataUnpackerJsonMsgpackTSocketJsonMsgpackDialoguePeerVariant<<Concept>>CodecJsonMsgpackTCodecTTransportTSocket*1txQueue_0..*+type1txQueue_0..*+fields1*rxBuffer_1<<Concept>>CodecJsonMsgpack<<use>>TTransportslot_callee_10..1registry_0..*callee_0..1impl_0..1slot_1TTransportreadership_slot_0..*10..1slot_1subscriber_1readership_0..*DialoguePeerEstablisherTCodecTTransportTCodecTTransportTSocketTEstablisherTEstablisher1..*1impl_0..1RawsockConnectorTCodecTEndpoint0..1<<Concept>>CodecUdsPathTCodecTTransportTCodecTTransportTSocketTEstablisherTEstablisherTCodecTEndpointimpl_0..111..*0..111` | `TResult function(TArgs...)` | +| [Basic Coroutine Call Slot](@ref BasicCoroutineCallSlots) | `wamp::basicCoroRpc` | `TResult function(TArgs..., Yield)` | +| [Unpacked Call Slot](@ref UnpackedCallSlots) | `wamp::unpackedRpc` | `Outcome function(Invocation, TArgs...)` | +| [Unpacked Coroutine Call Slot](@ref UnpackedCoroutineCallSlots) | `wamp::unpackedCoroRpc` | `Outcome function(Invocation, TArgs..., Yield)` | + +where `Yield` represents the type `boost::asio::yield_context`. + + @section BasicCallSlots Basic Call Slots + A _basic call slot_ represents an RPC handler that expects one or more payload arguments having specific, static types. The [wamp::basicRpc] (@ref wamp::BasicInvocationUnpacker::basicRpc) function can be used when @@ -93,7 +109,69 @@ passes them to the slot's argument list. If `Session` cannot convert the invocation payload arguments to their target types, it automatically sends an `ERROR` reply back to the router. + +@section BasicCoroutineCallSlots Basic Coroutine Call Slots + +A _basic coroutine call slot_ is like a regular _basic call slot_, except +that it is executed within the context of a coroutine. This is useful for +RPC handlers that need to perform asynchronous operations themselves. The +[wamp::basicCoroRpc](@ref wamp::BasicCoroInvocationUnpacker::basicCoroRpc) +function can be used when registering such call slots. It takes a basic +coroutine call slot, and converts it into a regular call slot that can be +passed to wamp::Session::enroll. + +`wamp::basicCoroRpc` expects a call slot with the following signature: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +TResult function(TArgs..., boost::asio::yield_context) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +where +- `TResult` is a static return type that will be automatically converted + to a single wamp::Result payload argument and then returned as a + wamp::Outcome. This return type may be `void` if the RPC is not expected + to return a value. +- `TArgs...` matches the template parameter pack that was passed to + `wamp::basicCoroRpc`. +- `boost::asio::yield_context` represents the RPC's coroutine context. + +Examples of basic coroutine call slots are: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +using Yield = boost::asio::yield_context; + +void setSpeed(float speed, Yield yield) { ... } +// ^ ^ +// TResult TArgs + +int purchase(std::string item, int cost, int qty, Yield yield) { ... } +//^ ^ ^ ^ +// \ \----------|---------/ +// TResult TArgs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The above slots can be registered as follows: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +session->enroll(Procedure("setSpeed"), + basicCoroRpc(&setSpeed), + handler); + +session->enroll(Procedure("purchase"), + basicCoroRpc(&purchase), + handler); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +where `handler` is the asynchronous handler (or coroutine yield context) +for the **enroll operation itself**. + +Whenever a wamp::Session dispatches an RPC invocation to the above slots, it +spawns a new coroutine to be executed on wamp::Session::userIosvc(). It then +automatically unpacks the invocation payload positional arguments, and passes +them to the slot's argument list. If `Session` cannot convert the invocation +payload arguments to their target types, it automatically sends an `ERROR` +reply back to the router. + + @section UnpackedCallSlots Unpacked Call Slots + An _unpacked call slot_ represents an RPC handler that expects one or more payload arguments having specific, static types. The [wamp::unpackedRpc] (@ref wamp::InvocationUnpacker::unpackedRpc) function can be used when @@ -155,6 +233,30 @@ passes them to the slot's argument list. If `Session` cannot convert the invocation payload arguments to their target types, it automatically sends an `ERROR` reply back to the router. + +@section UnpackedCoroutineCallSlots Unpacked Coroutine Call Slots + +An _unpacked coroutine call slot_ is like an +[unpacked call slot](@ref UnpackedCallSlots), except that the slot is +executed within the context of a coroutine. The [wamp::unpackedCoroRpc] +(@ref wamp::CoroInvocationUnpacker::unpackedCoroRpc) function can be used when +registering such call slots. + +`wamp::unpackedCoroRpc` expects a call slot with the following signature: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +wamp::Outcome function(wamp::Invocation, TArgs..., boost::asio::yield_context) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +where +- wamp::Outcome contains a wamp::Result or a wamp::Error object to be sent back + to the caller, +- wamp::Invocation is an object containing information and payload arguments + related to the RPC invocation, and, +- `TArgs...` matches the template parameter pack that was passed to + `wamp::unpackedRpc`. +- `boost::asio::yield_context` represents the RPC's coroutine context. + + @section RpcOutcomes RPC Outcomes RPC call slots are required to return a wamp::Outcome object. `Outcome` is a @@ -243,14 +345,15 @@ Outcome Client::addPerson(Invocation inv, Object person) { // We need to issue another RPC to the database service before we can // fulfill this RPC. - boost::asio::spawn(iosvc_, [this, inv](boost::asio::yield_context yield) - { - auto dbResult = session_->call( Rpc("db.add").withArgs(person), - yield ); - // Manually send the result back to the caller - auto personId = dbResult[0].to(); - inv.yield({personId}); - }); + boost::asio::spawn(inv.iosvc(), + [this, inv](boost::asio::yield_context yield) + { + auto dbResult = session_->call( Rpc("db.add").withArgs(person), + yield ); + // Manually send the result back to the caller + auto personId = dbResult[0].to(); + inv.yield({personId}); + }); // We don't know the result yet as this point. Return a deferred outcome // to indicate that we'll send the result manually in a different @@ -259,6 +362,7 @@ Outcome Client::addPerson(Invocation inv, Object person) } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @section ScopedRegistrations Scoped Registrations A wamp::ScopedRegistration object can be used to limit a registration's diff --git a/doc/subscriptions.dox b/doc/subscriptions.dox index d3bf8095..f62e6c0c 100644 --- a/doc/subscriptions.dox +++ b/doc/subscriptions.dox @@ -11,7 +11,9 @@ @tableofcontents + @section EventSlots Event Slots + A _slot_ is a _callable target_ that is called in response to a _signal_ (the signal being the event topic in this case). The term _slot_, borrowed from [Qt's signals and slots][qt_sig], is used to distinguish an event handler @@ -38,8 +40,21 @@ void function(wamp::Event) where wamp::Event is an object containing information and payload arguments related to the publication. +The following table summarizes the different types of event slots that can +be used with the library: + +| Slot Type | Wrapper Function | Slot Signature | +|-------------------------------------------------------------------|-------------------------------------|-----------------------------------------| +| [Basic Event Slot](@ref BasicEventSlots) | `wamp::basicEvent` | `void function(TArgs...)` | +| [Basic Coroutine Event Slot](@ref BasicCoroutineEventSlots) | `wamp::basicCoroEvent` | `void function(TArgs..., Yield)` | +| [Unpacked Event Slot](@ref UnpackedEventSlots) | `wamp::unpackedEvent` | `void function(Event, TArgs...)` | +| [Unpacked Coroutine Event Slot](@ref UnpackedCoroutineEventSlots) | `wamp::unpackedCoroEvent` | `void function(Event, TArgs..., Yield)` | + +where `Yield` represents the type `boost::asio::yield_context`. + @section BasicEventSlots Basic Event Slots -An _basic event slot_ represents an event handler that expects one or more + +A _basic event slot_ represents an event handler that expects one or more payload arguments having specific, static types. The [wamp::basicEvent] (@ref wamp::BasicEventUnpacker::basicEvent) function can be used when registering such event slots. It takes a basic event slot, and converts it @@ -85,7 +100,65 @@ them to the slot's argument list. If `Session` cannot convert the event payload arguments to their target types, it issues a warning that can be captured via wamp::Session::setWarningHandler. + +@section BasicCoroutineEventSlots Basic Coroutine Event Slots + +A _basic coroutine event slot_ is like a regular _basic event slot_, except +that it is executed within the context of a coroutine. This is useful for +event handlers that need to perform asynchronous operations themselves. The +[wamp::basicCoroEvent](@ref wamp::BasicCoroEventUnpacker::basicCoroEvent) +function can be used when registering such event slots. It takes a basic +coroutine event slot, and converts it into a regular event slot that can be +passed to wamp::Session::subscribe. + +`wamp::basicCoroEvent` expects an event slot with the following signature: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void function(TArgs..., boost::asio::yield_context) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +where +- `TArgs...` matches the template parameter pack that was passed to + `wamp::basicEvent`. +- `boost::asio::yield_context` represents the event handler's coroutine context. + +Examples of basic coroutine event slots are: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +using Yield = boost::asio::yield_context; + +void onSensorSampled(float value, Yield yield) { ... } +// ^ +// TArgs + +void onPurchase(std::string item, int cost, int qty, Yield yield) { ... } +// ^ ^ ^ +// \----------|----------/ +// TArgs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The above slots can be registered as follows: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +session->subscribe(Topic("sensorSampled"), + basicCoroEvent(&onSensorSampled), + handler); + +session->subscribe(Topic("itemPurchased"), + basicCoroEvent(&onPurchase), + handler); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +where `handler` is the asynchronous handler (or coroutine yield context) +for the **subscribe operation itself**. + +Whenever a wamp::Session dispatches an event to the above slots, it +spawns a new coroutine to be executed on wamp::Session::userIosvc(). It then +automatically unpacks the event payload positional arguments, and passes +them to the slot's argument list. If `Session` cannot convert the event payload +arguments to their target types, it issues a warning that can be captured via +wamp::Session::setWarningHandler. + + @section UnpackedEventSlots Unpacked Event Slots + An _unpacked event slot_ represents an event handler that expects one or more payload arguments having specific, static types. The [wamp::unpackedEvent] (@ref wamp::EventUnpacker::unpackedEvent) function can be used when @@ -139,6 +212,28 @@ them to the slot's argument list. If `Session` cannot convert the event payload arguments to their target types, it issues a warning that can be captured via wamp::Session::setWarningHandler. + +@section UnpackedCoroutineEventSlots Unpacked Coroutine Event Slots + +An _unpacked coroutine event slot_ is like an [unpacked event slot] +(@ref UnpackedEventSlots), except that the slot is +executed within the context of a coroutine. The [wamp::unpackedCoroEvent] +(@ref wamp::CoroEventUnpacker::unpackedCoroEvent) function can be used when +registering such event slots. + +`wamp::unpackedCoroEvent` expects an event slot with the following signature: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +void function(wamp::Event, TArgs..., boost::asio::yield_context) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +where +- wamp::Event is an object containing information and payload arguments related + to the publication, and, +- `TArgs...` matches the template parameter pack that was passed to + `wamp::unpackedEvent`. +- `boost::asio::yield_context` represents the event handler's coroutine context. + + @section ScopedSubscriptions Scoped Subscriptions A wamp::ScopedSubscription object can be used to limit a subscription's diff --git a/examples/chat/main.cpp b/examples/chat/main.cpp index 7dbe8fdc..50219837 100644 --- a/examples/chat/main.cpp +++ b/examples/chat/main.cpp @@ -21,10 +21,14 @@ class ChatService public: using Yield = boost::asio::yield_context; + explicit ChatService(wamp::AsioService& iosvc) + : iosvc_(iosvc) + {} + void start(wamp::ConnectorList connectors, Yield yield) { using namespace wamp; - session_ = CoroSession<>::create(connectors); + session_ = CoroSession<>::create(iosvc_, connectors); auto index = session_->connect(yield); std::cout << "Chat service connected on transport #" @@ -57,6 +61,7 @@ class ChatService session_->publish( wamp::Pub("said").withArgs(user, message) ); } + wamp::AsioService& iosvc_; wamp::CoroSession<>::Ptr session_; wamp::ScopedRegistration registration_; }; @@ -68,12 +73,15 @@ class ChatClient public: using Yield = boost::asio::yield_context; - explicit ChatClient(std::string user) : user_(std::move(user)) {} + ChatClient(wamp::AsioService& iosvc, std::string user) + : iosvc_(iosvc), + user_(std::move(user)) + {} void join(wamp::ConnectorList connectors, Yield yield) { using namespace wamp; - session_ = CoroSession<>::create(connectors); + session_ = CoroSession<>::create(iosvc_, connectors); auto index = session_->connect(yield); std::cout << user_ << " connected on transport #" << index << "\n"; @@ -109,6 +117,7 @@ class ChatClient << message << "\"\n"; } + wamp::AsioService& iosvc_; std::string user_; wamp::CoroSession<>::Ptr session_; wamp::ScopedSubscription subscription_; @@ -120,19 +129,14 @@ int main() { wamp::AsioService iosvc; -#ifdef CPPWAMP_USE_LEGACY_CONNECTORS - auto tcp = wamp::legacyConnector( iosvc, - wamp::TcpHost("localhost", 12345) ); -#else auto tcp = wamp::connector( iosvc, wamp::TcpHost("localhost", 12345) ); -#endif // Normally, the service and client instances would be in separate programs. // We run them all here in the same coroutine for demonstration purposes. - ChatService chat; - ChatClient alice("Alice"); - ChatClient bob("Bob"); + ChatService chat(iosvc); + ChatClient alice(iosvc, "Alice"); + ChatClient bob(iosvc, "Bob"); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { diff --git a/examples/timeclient/main.cpp b/examples/timeclient/main.cpp index a8b44d2c..9da31644 100644 --- a/examples/timeclient/main.cpp +++ b/examples/timeclient/main.cpp @@ -45,17 +45,11 @@ void onTimeTick(std::tm time) //------------------------------------------------------------------------------ int main() { - wamp::AsioService iosvc; - -#ifdef CPPWAMP_USE_LEGACY_CONNECTORS - auto tcp = wamp::legacyConnector(iosvc, - wamp::TcpHost(address, port)); -#else - auto tcp = wamp::connector(iosvc, wamp::TcpHost(address, port)); -#endif - using namespace wamp; - auto session = CoroSession<>::create(tcp); + + AsioService iosvc; + auto tcp = connector(iosvc, TcpHost(address, port)); + auto session = CoroSession<>::create(iosvc, tcp); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { diff --git a/examples/timeservice/main.cpp b/examples/timeservice/main.cpp index 7a659e26..e4ba13e9 100644 --- a/examples/timeservice/main.cpp +++ b/examples/timeservice/main.cpp @@ -47,19 +47,11 @@ std::tm getTime() //------------------------------------------------------------------------------ int main() { - wamp::AsioService iosvc; - -#ifdef CPPWAMP_USE_LEGACY_CONNECTORS - auto uds = wamp::legacyConnector(iosvc, - wamp::UdsPath(udsPath)); -#else - auto uds1 = wamp::connector(iosvc, wamp::UdsPath(udsPath1)); - auto uds2 = wamp::connector(iosvc, wamp::UdsPath(udsPath2)); -#endif - using namespace wamp; - auto session = CoroSession<>::create({uds1, uds2}); - + AsioService iosvc; + auto uds1 = connector(iosvc, UdsPath(udsPath1)); + auto uds2 = connector(iosvc, UdsPath(udsPath2)); + auto session = CoroSession<>::create(iosvc, {uds1, uds2}); boost::asio::steady_timer timer(iosvc); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0e60d2d8..2db1a64d 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -8,7 +8,6 @@ set(SOURCES codectestjson.cpp codectestmsgpack.cpp - legacytransporttest.cpp payloadtest.cpp transporttest.cpp varianttestassign.cpp diff --git a/test/legacytransporttest.cpp b/test/legacytransporttest.cpp deleted file mode 100644 index 0fc4609c..00000000 --- a/test/legacytransporttest.cpp +++ /dev/null @@ -1,325 +0,0 @@ -/*------------------------------------------------------------------------------ - Copyright Butterfly Energy Systems 2014-2015. - Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at - http://www.boost.org/LICENSE_1_0.txt) -------------------------------------------------------------------------------*/ - -#if CPPWAMP_TESTING_TRANSPORT - -#include -#include -#include -#include -#include -#include -#include -#include "transporttest.hpp" - -using namespace wamp; - -using TcpAsioConnector = internal::LegacyAsioEndpoint; -using TcpAsioListener = internal::LegacyAsioEndpoint; -using UdsAsioConnector = internal::LegacyAsioEndpoint; -using UdsAsioListener = internal::LegacyAsioEndpoint; -using TcpTransport = TcpAsioConnector::Transport; -using UdsTransport = UdsAsioConnector::Transport; - -namespace -{ - -//------------------------------------------------------------------------------ -struct TcpLoopbackFixture : - protected LoopbackFixtureBase, - public LoopbackFixture -{ - TcpLoopbackFixture( - bool connected = true, - int codec = Json::id(), - RawsockMaxLength clientMaxRxLength = RawsockMaxLength::kB_64, - RawsockMaxLength serverMaxRxLength = RawsockMaxLength::kB_64 ) - : LoopbackFixture( - clientService, - serverService, - internal::TcpOpener(clientService, {tcpLoopbackAddr, tcpTestPort}), - codec, - clientMaxRxLength, - internal::TcpAcceptor(serverService, tcpTestPort), - codec, - serverMaxRxLength, - connected ) - {} -}; - -//------------------------------------------------------------------------------ -struct UdsLoopbackFixture : - protected LoopbackFixtureBase, - public LoopbackFixture -{ - UdsLoopbackFixture( - bool connected = true, - int codec = Json::id(), - RawsockMaxLength clientMaxRxLength = RawsockMaxLength::kB_64, - RawsockMaxLength serverMaxRxLength = RawsockMaxLength::kB_64 ) - : LoopbackFixture( - clientService, - serverService, - internal::UdsOpener(clientService, {udsTestPath}), - codec, - clientMaxRxLength, - internal::UdsAcceptor(serverService, udsTestPath, true), - codec, - serverMaxRxLength, - connected ) - {} -}; - -//------------------------------------------------------------------------------ -template -void checkReceiveTooLong(TFixture& f, - typename TFixture::TransportPtr sender, - typename TFixture::TransportPtr receiver) -{ - using Transport = typename TFixture::Transport; - using Buffer = typename Transport::Buffer; - - bool receiveFailed = false; - receiver->start( - [&](Buffer) - { - FAIL( "unexpected receive" ); - }, - [&](std::error_code ec) - { - CHECK( ec == TransportErrc::badRxLength ); - receiveFailed = true; - }); - - sender->start( - [&](Buffer) - { - FAIL( "unexpected receive" ); - }, - [&](std::error_code) {} ); - - auto sendBuf = sender->getBuffer(); - std::string tooLong(receiver->maxReceiveLength() + 1, 'a'); - sendBuf->write(tooLong.data(), tooLong.size()); - sender->send(std::move(sendBuf)); - - REQUIRE_NOTHROW( f.run() ); - CHECK( receiveFailed ); -} - -} // anonymous namespace - - -//------------------------------------------------------------------------------ -SCENARIO( "Normal legacy connection", "[Transport]" ) -{ -GIVEN( "an unconnected TCP connector/listener pair" ) -{ - WHEN( "the client and server use JSON" ) - { - TcpLoopbackFixture f(false, Json::id()); - checkConnection(f, Json::id()); - } - WHEN( "the client and server use Msgpack" ) - { - TcpLoopbackFixture f(false, Msgpack::id()); - checkConnection(f, Msgpack::id()); - } -} -GIVEN( "an unconnected UDS connector/listener pair" ) -{ - WHEN( "the client and server use JSON" ) - { - UdsLoopbackFixture f(false, Json::id()); - checkConnection(f, Json::id()); - } - WHEN( "the client and server use Msgpack" ) - { - UdsLoopbackFixture f(false, Msgpack::id()); - checkConnection(f, Msgpack::id()); - } -} -} - -//------------------------------------------------------------------------------ -SCENARIO( "Normal legacy communications", "[Transport,Legacy]" ) -{ -GIVEN( "a connected client/server TCP transport pair" ) -{ - TcpLoopbackFixture f; - checkCommunications(f); -} -GIVEN( "a connected client/server UDS transport pair" ) -{ - UdsLoopbackFixture f; - checkCommunications(f); -} -} - -//------------------------------------------------------------------------------ -SCENARIO( "Consecutive legacy send/receive", "[Transport,Legacy]" ) -{ -GIVEN( "a connected client/server TCP transport pair" ) -{ - { - TcpLoopbackFixture f; - checkConsecutiveSendReceive(f, f.client, f.server); - } - { - TcpLoopbackFixture f; - checkConsecutiveSendReceive(f, f.server, f.client); - } -} -GIVEN( "a connected client/server UDS transport pair" ) -{ - { - UdsLoopbackFixture f; - checkConsecutiveSendReceive(f, f.client, f.server); - } - { - UdsLoopbackFixture f; - checkConsecutiveSendReceive(f, f.server, f.client); - } -} -} - -//------------------------------------------------------------------------------ -SCENARIO( "Maximum length legacy messages", "[Transport]" ) -{ -GIVEN( "a connected client/server TCP transport pair" ) -{ - TcpLoopbackFixture f; - const std::string message(f.client->maxReceiveLength(), 'm'); - const std::string reply(f.server->maxReceiveLength(), 'r');; - checkSendReply(f, message, reply); -} -GIVEN( "a connected client/server UDS transport pair" ) -{ - UdsLoopbackFixture f; - const std::string message(f.client->maxReceiveLength(), 'm'); - const std::string reply(f.server->maxReceiveLength(), 'r');; - checkSendReply(f, message, reply); -} -} - -//------------------------------------------------------------------------------ -SCENARIO( "Zero length legacy messages", "[Transport,Legacy]" ) -{ -const std::string message(""); -const std::string reply(""); - -GIVEN( "a connected client/server TCP transport pair" ) -{ - TcpLoopbackFixture f; - checkSendReply(f, message, reply); -} -GIVEN( "a connected client/server UDS transport pair" ) -{ - UdsLoopbackFixture f; - checkSendReply(f, message, reply); -} -} - -//------------------------------------------------------------------------------ -SCENARIO( "Cancel legacy listen", "[Transport,Legacy]" ) -{ - GIVEN( "an unconnected TCP listener/connector pair" ) - { - TcpLoopbackFixture f(false); - checkCancelListen(f); - checkConnection(f, Json::id()); - checkSendReply(f, "Hello", "World"); - } - GIVEN( "an unconnected UDS listener/connector pair" ) - { - UdsLoopbackFixture f(false); - checkCancelListen(f); - checkConnection(f, Json::id()); - checkSendReply(f, "Hello", "World"); - } -} - -//------------------------------------------------------------------------------ -SCENARIO( "Cancel legacy connect", "[Transport,Legacy]" ) -{ - GIVEN( "an unconnected TCP listener/connector pair" ) - { - TcpLoopbackFixture f(false); - checkCancelConnect(f); - } - GIVEN( "an unconnected UDS listener/connector pair" ) - { - UdsLoopbackFixture f(false); - checkCancelConnect(f); - } -} - -//------------------------------------------------------------------------------ -SCENARIO( "Cancel legacy receive", "[Transport,Legacy]" ) -{ - GIVEN( "a connected TCP listener/connector pair" ) - { - TcpLoopbackFixture f; - checkCancelReceive(f); - } - GIVEN( "a connected UDS listener/connector pair" ) - { - UdsLoopbackFixture f; - checkCancelReceive(f); - } -} - -//------------------------------------------------------------------------------ -SCENARIO( "Cancel legacy send", "[Transport]" ) -{ - // The size of transmission is set to maximum to increase the likelyhood - // of the operation being aborted, rather than completed. - - GIVEN( "a connected TCP listener/connector pair" ) - { - TcpLoopbackFixture f(false, Json::id(), RawsockMaxLength::MB_16, - RawsockMaxLength::MB_16); - checkCancelSend(f); - } - GIVEN( "a connected UDS listener/connector pair" ) - { - UdsLoopbackFixture f(false, Json::id(), RawsockMaxLength::MB_16, - RawsockMaxLength::MB_16); - checkCancelSend(f); - } -} - -//------------------------------------------------------------------------------ -SCENARIO( "Receiving legacy messages longer than maximum", "[Transport]" ) -{ -GIVEN ( "A TCP client that sends an overly long message" ) -{ - TcpLoopbackFixture f(true, Json::id(), RawsockMaxLength::kB_64, - RawsockMaxLength::kB_32); - checkReceiveTooLong(f, f.client, f.server); -} -GIVEN ( "A TCP server that sends an overly long message" ) -{ - TcpLoopbackFixture f(true, Json::id(), RawsockMaxLength::kB_32, - RawsockMaxLength::kB_64); - checkReceiveTooLong(f, f.server, f.client); -} -GIVEN ( "A UDS client that sends an overly long message" ) -{ - UdsLoopbackFixture f(true, Json::id(), RawsockMaxLength::kB_64, - RawsockMaxLength::kB_32); - checkReceiveTooLong(f, f.client, f.server); -} -GIVEN ( "A UDS server that sends an overly long message" ) -{ - UdsLoopbackFixture f(true, Json::id(), RawsockMaxLength::kB_32, - RawsockMaxLength::kB_64); - checkReceiveTooLong(f, f.server, f.client); -} -} - -#endif // #if CPPWAMP_TESTING_TRANSPORT diff --git a/test/test.pro b/test/test.pro index 81f9c64c..b55451ee 100644 --- a/test/test.pro +++ b/test/test.pro @@ -13,7 +13,6 @@ CONFIG -= qt SOURCES += \ codectestjson.cpp \ codectestmsgpack.cpp \ - legacytransporttest.cpp \ payloadtest.cpp \ transporttest.cpp \ varianttestassign.cpp \ diff --git a/test/varianttestconvertcontainers.cpp b/test/varianttestconvertcontainers.cpp index ef371d15..ea51f5c0 100644 --- a/test/varianttestconvertcontainers.cpp +++ b/test/varianttestconvertcontainers.cpp @@ -7,8 +7,11 @@ #if CPPWAMP_TESTING_VARIANT +#include #include +#include #include +#include using namespace wamp; @@ -68,12 +71,12 @@ GIVEN( "an empty std::unordered_map" ) auto v = Variant::from(map); THEN( "the variant is as expected" ) { - CHECK( v.is() ); + REQUIRE( v.is() ); CHECK( v.as().empty() ); } } } -GIVEN( "a invalid variant object type" ) +GIVEN( "an invalid variant object type" ) { Variant v(Object{{"a", 1},{"b", null}}); WHEN( "converting to a std::unordered_map" ) @@ -84,4 +87,143 @@ GIVEN( "a invalid variant object type" ) } } + +//------------------------------------------------------------------------------ +SCENARIO( "Converting to/from std::set", "[Variant]" ) +{ +GIVEN( "a valid variant array type" ) +{ + Variant v(Array{1, 3, 2}); + WHEN( "converting to a std::set" ) + { + auto set = v.to>(); + THEN( "the set is as expected" ) + { + REQUIRE( set.size() == 3 ); + auto iter = set.begin(); + CHECK( *iter++ == 1 ); + CHECK( *iter++ == 2 ); + CHECK( *iter++ == 3 ); + } + } +} +GIVEN( "an empty variant array type" ) +{ + Variant v(Array{}); + WHEN( "converting to a std::set" ) + { + auto set = v.to>(); + THEN( "the set is as expected" ) + { + CHECK( set.empty() ); + } + } +} +GIVEN( "a valid std::set type" ) +{ + std::set set{"a", "b", "c"}; + WHEN( "converting to a variant" ) + { + auto v = Variant::from(set); + THEN( "the variant is as expected" ) + { + CHECK( v == (Array{"a", "b", "c"}) ); + } + } +} +GIVEN( "an empty std::set" ) +{ + std::set set{}; + WHEN( "converting to a variant" ) + { + auto v = Variant::from(set); + THEN( "the variant is as expected" ) + { + REQUIRE( v.is() ); + CHECK( v.as().empty() ); + } + } +} +GIVEN( "an invalid variant array type" ) +{ + Variant v(Array{"a", null}); + WHEN( "converting to a std::set" ) + { + using SetType = std::set; + CHECK_THROWS_AS( v.to(), error::Conversion ); + } +} +} + + +//------------------------------------------------------------------------------ +SCENARIO( "Converting to/from std::unordered_set", "[Variant]" ) +{ +GIVEN( "a valid variant array type" ) +{ + Variant v(Array{1, 3, 2}); + WHEN( "converting to a std::unordered_set" ) + { + auto set = v.to>(); + THEN( "the set is as expected" ) + { + REQUIRE( set.size() == 3 ); + std::set sorted(set.begin(), set.end()); + auto iter = sorted.begin(); + CHECK( *iter++ == 1 ); + CHECK( *iter++ == 2 ); + CHECK( *iter++ == 3 ); + } + } +} +GIVEN( "an empty variant array type" ) +{ + Variant v(Array{}); + WHEN( "converting to a std::unordered_set" ) + { + auto set = v.to>(); + THEN( "the set is as expected" ) + { + CHECK( set.empty() ); + } + } +} +GIVEN( "a valid std::unordered_set type" ) +{ + std::unordered_set set{"a", "b", "c"}; + WHEN( "converting to a variant" ) + { + auto v = Variant::from(set); + THEN( "the variant is as expected" ) + { + auto array = v.as(); + std::sort(array.begin(), array.end()); + CHECK( array == (Array{"a", "b", "c"}) ); + } + } +} +GIVEN( "an empty std::unordered_set" ) +{ + std::unordered_set set{}; + WHEN( "converting to a variant" ) + { + auto v = Variant::from(set); + THEN( "the variant is as expected" ) + { + REQUIRE( v.is() ); + CHECK( v.as().empty() ); + } + } +} +GIVEN( "an invalid variant array type" ) +{ + Variant v(Array{"a", null}); + WHEN( "converting to a std::unordered_set" ) + { + using SetType = std::unordered_set; + CHECK_THROWS_AS( v.to(), error::Conversion ); + } +} +} + #endif // #if CPPWAMP_TESTING_VARIANT diff --git a/test/wamptest.cpp b/test/wamptest.cpp index 1ad19fe0..f28e1d4f 100644 --- a/test/wamptest.cpp +++ b/test/wamptest.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -34,32 +35,19 @@ const std::string testUdsPath = "./.crossbar/udstest"; Connector::Ptr tcp(AsioService& iosvc) { -#ifdef CPPWAMP_USE_LEGACY_CONNECTORS - return legacyConnector(iosvc, TcpHost("localhost", validPort)); -#else return connector(iosvc, TcpHost("localhost", validPort)); -#endif } Connector::Ptr invalidTcp(AsioService& iosvc) { -#ifdef CPPWAMP_USE_LEGACY_CONNECTORS - return legacyConnector(iosvc, TcpHost("localhost", invalidPort)); -#else return connector(iosvc, TcpHost("localhost", invalidPort)); -#endif } - #if CPPWAMP_HAS_UNIX_DOMAIN_SOCKETS - Connector::Ptr udsMsgpack(AsioService& iosvc) - { - #ifdef CPPWAMP_USE_LEGACY_CONNECTORS - return legacyConnector(iosvc, UdsPath(testUdsPath)); - #else - return connector(iosvc, UdsPath(testUdsPath)); - #endif - } +Connector::Ptr udsMsgpack(AsioService& iosvc) +{ + return connector(iosvc, UdsPath(testUdsPath)); +} #endif @@ -69,10 +57,11 @@ struct PubSubFixture using PubVec = std::vector; template - PubSubFixture(TConnector cnct) - : publisher(CoroSession<>::create(cnct)), - subscriber(CoroSession<>::create(cnct)), - otherSubscriber(CoroSession<>::create(cnct)) + PubSubFixture(AsioService& iosvc, TConnector cnct) + : iosvc(iosvc), + publisher(CoroSession<>::create(iosvc, cnct)), + subscriber(CoroSession<>::create(iosvc, cnct)), + otherSubscriber(CoroSession<>::create(iosvc, cnct)) {} void join(boost::asio::yield_context yield) @@ -110,6 +99,7 @@ struct PubSubFixture { INFO( "in onDynamicEvent" ); CHECK( event.pubId() <= 9007199254740992ull ); + CHECK( &event.iosvc() == &iosvc ); dynamicArgs = event.args(); dynamicPubs.push_back(event.pubId()); } @@ -118,6 +108,7 @@ struct PubSubFixture { INFO( "in onStaticEvent" ); CHECK( event.pubId() <= 9007199254740992ull ); + CHECK( &event.iosvc() == &iosvc ); staticArgs = Array{{str, num}}; staticPubs.push_back(event.pubId()); } @@ -126,9 +117,12 @@ struct PubSubFixture { INFO( "in onOtherEvent" ); CHECK( event.pubId() <= 9007199254740992ull ); + CHECK( &event.iosvc() == &iosvc ); otherPubs.push_back(event.pubId()); } + AsioService& iosvc; + CoroSession<>::Ptr publisher; CoroSession<>::Ptr subscriber; CoroSession<>::Ptr otherSubscriber; @@ -149,9 +143,10 @@ struct PubSubFixture struct RpcFixture { template - RpcFixture(TConnector cnct) - : caller(CoroSession<>::create(cnct)), - callee(CoroSession<>::create(cnct)) + RpcFixture(AsioService& iosvc, TConnector cnct) + : iosvc(iosvc), + caller(CoroSession<>::create(iosvc, cnct)), + callee(CoroSession<>::create(iosvc, cnct)) {} void join(boost::asio::yield_context yield) @@ -181,6 +176,7 @@ struct RpcFixture { INFO( "in RPC handler" ); CHECK( inv.requestId() <= 9007199254740992ull ); + CHECK( &inv.iosvc() == &iosvc ); ++dynamicCount; // Echo back the call arguments as the result. return Result().withArgList(inv.args()); @@ -190,11 +186,14 @@ struct RpcFixture { INFO( "in RPC handler" ); CHECK( inv.requestId() <= 9007199254740992ull ); + CHECK( &inv.iosvc() == &iosvc ); ++staticCount; // Echo back the call arguments as the yield result. return {str, num}; } + AsioService& iosvc; + CoroSession<>::Ptr caller; CoroSession<>::Ptr callee; @@ -213,7 +212,7 @@ void checkInvalidUri(TThrowDelegate&& throwDelegate, AsioService iosvc; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(tcp(iosvc)); + auto session = CoroSession<>::create(iosvc, tcp(iosvc)); session->connect(yield); if (joined) session->join(Realm(testRealm), yield); @@ -242,7 +241,7 @@ void checkDisconnect(TDelegate&& delegate) AsyncResult result; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(tcp(iosvc)); + auto session = CoroSession<>::create(iosvc, tcp(iosvc)); session->connect(yield); delegate(*session, yield, completed, result); session->disconnect(); @@ -311,7 +310,7 @@ void checkInvalidOps(CoroSession<>::Ptr session, CHECK_THROWS_AS( session->call(Rpc("rpc"), [](AsyncResult) {}), error::Logic ); CHECK_THROWS_AS( session->call(Rpc("rpc").withArgs(42), - [](AsyncResult) {}), + [](AsyncResult) {}), error::Logic ); CHECK_THROWS_AS( session->leave(Reason(), yield), error::Logic ); @@ -361,7 +360,7 @@ GIVEN( "an IO service and a TCP connector" ) { { // Connect and disconnect a session-> - auto s = CoroSession<>::create(cnct); + auto s = CoroSession<>::create(iosvc, cnct); CHECK( s->state() == SessionState::disconnected ); CHECK( s->connect(yield) == 0 ); CHECK( s->state() == SessionState::closed ); @@ -380,7 +379,7 @@ GIVEN( "an IO service and a TCP connector" ) } // Check that another client can connect and disconnect. - auto s2 = CoroSession<>::create(cnct); + auto s2 = CoroSession<>::create(iosvc, cnct); CHECK( s2->state() == SessionState::disconnected ); CHECK( s2->connect(yield) == 0 ); CHECK( s2->state() == SessionState::closed ); @@ -393,7 +392,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "joining and leaving" ) { - auto s = CoroSession<>::create(cnct); + auto s = CoroSession<>::create(iosvc, cnct); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { s->connect(yield); @@ -449,7 +448,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "connecting, joining, leaving, and disconnecting" ) { - auto s = CoroSession<>::create(cnct); + auto s = CoroSession<>::create(iosvc, cnct); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { { @@ -507,7 +506,8 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "disconnecting during connect" ) { std::error_code ec; - auto s = Session::create(ConnectorList({invalidTcp(iosvc), cnct})); + auto s = Session::create(iosvc, + ConnectorList({invalidTcp(iosvc), cnct})); bool connectHandlerInvoked = false; s->connect([&](AsyncResult result) { @@ -547,7 +547,7 @@ GIVEN( "an IO service and a TCP connector" ) { std::error_code ec; bool connected = false; - auto s = CoroSession<>::create(cnct); + auto s = CoroSession<>::create(iosvc, cnct); bool disconnectTriggered = false; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { @@ -580,7 +580,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "resetting during connect" ) { bool handlerWasInvoked = false; - auto s = Session::create(cnct); + auto s = Session::create(iosvc, cnct); s->connect([&handlerWasInvoked](AsyncResult) { handlerWasInvoked = true; @@ -594,7 +594,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "resetting during join" ) { bool handlerWasInvoked = false; - auto s = Session::create(cnct); + auto s = Session::create(iosvc, cnct); s->connect([&](AsyncResult) { s->join(Realm(testRealm), [&](AsyncResult) @@ -612,7 +612,7 @@ GIVEN( "an IO service and a TCP connector" ) { bool handlerWasInvoked = false; - auto session = Session::create(cnct); + auto session = Session::create(iosvc, cnct); std::weak_ptr weakClient(session); session->connect([&handlerWasInvoked](AsyncResult) @@ -646,7 +646,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "joining and leaving" ) { - auto s = CoroSession<>::create(cnct); + auto s = CoroSession<>::create(iosvc, cnct); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { s->connect(yield); @@ -715,7 +715,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -804,7 +804,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.staticSub = f.subscriber->subscribe( Topic("str.num"), @@ -838,7 +838,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -885,7 +885,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -919,7 +919,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -955,7 +955,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -991,7 +991,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -1021,7 +1021,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); f.join(yield); f.subscribe(yield); @@ -1084,7 +1084,7 @@ GIVEN( "an IO service and a TCP connector" ) { Result result; std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1134,7 +1134,7 @@ GIVEN( "an IO service and a TCP connector" ) { Result result; std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1183,12 +1183,12 @@ GIVEN( "an IO service and a TCP connector" ) { Result result; std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.staticReg = f.callee->enroll( Procedure("static"), - basicRpc([&](std::string s, int n) + basicRpc([&](std::string, int n) { ++f.staticCount; return n; // Echo back the integer argument @@ -1224,7 +1224,7 @@ GIVEN( "an IO service and a TCP connector" ) using namespace std::placeholders; f.staticReg = f.callee->enroll( Procedure("static"), - basicRpc([&](std::string s, int n) + basicRpc([&](std::string, int n) { ++f.staticCount; return n; // Echo back the integer argument @@ -1252,7 +1252,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1287,7 +1287,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1324,7 +1324,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1361,7 +1361,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1391,7 +1391,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1435,10 +1435,12 @@ SCENARIO( "Nested WAMP RPCs and Events", "[WAMP]" ) { GIVEN( "these test fixture objects" ) { + using Yield = boost::asio::yield_context; + AsioService iosvc; auto cnct = tcp(iosvc); - auto session1 = CoroSession<>::create(cnct); - auto session2 = CoroSession<>::create(cnct); + auto session1 = CoroSession<>::create(iosvc, cnct); + auto session2 = CoroSession<>::create(iosvc, cnct); // Regular RPC handler auto upperify = [](Invocation, std::string str) -> Outcome @@ -1450,22 +1452,14 @@ GIVEN( "these test fixture objects" ) WHEN( "calling remote procedures within an invocation" ) { - auto uppercat = [&iosvc, session2](Invocation inv, std::string str1, - std::string str2) -> Outcome + auto uppercat = [session2](std::string str1, std::string str2, + boost::asio::yield_context yield) -> String { - // We need a separate yield context here - boost::asio::spawn(iosvc, [session2, inv, str1, str2] - (boost::asio::yield_context yield) - { - auto upper1 = session2->call( - Rpc("upperify").withArgs(str1), yield); - auto upper2 = session2->call( - Rpc("upperify").withArgs(str2), yield); - auto concatted = upper1[0].to() + - upper2[0].to(); - inv.yield(Result({concatted})); - }); - return Outcome::deferred(); + auto upper1 = session2->call( + Rpc("upperify").withArgs(str1), yield); + auto upper2 = session2->call( + Rpc("upperify").withArgs(str2), yield); + return upper1[0].to() + upper2[0].to(); }; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) @@ -1478,9 +1472,10 @@ GIVEN( "these test fixture objects" ) session2->connect(yield); session2->join(Realm(testRealm), yield); - session2->enroll(Procedure("uppercat"), - unpackedRpc(uppercat), - yield); + session2->enroll( + Procedure("uppercat"), + basicCoroRpc(uppercat), + yield); std::string s1 = "hello "; std::string s2 = "world"; @@ -1500,17 +1495,13 @@ GIVEN( "these test fixture objects" ) auto subscriber = session2; std::string upperized; - auto onEvent = [&iosvc, &upperized, subscriber] - (Event event, std::string str) + auto onEvent = + [&upperized, subscriber](std::string str, + boost::asio::yield_context yield) { - // We need a separate yield context here - boost::asio::spawn(iosvc, [&upperized, subscriber, event, str] - (boost::asio::yield_context yield) - { - auto result = subscriber->call(Rpc("upperify") - .withArgs(str), yield); - upperized = result[0].to(); - }); + auto result = subscriber->call(Rpc("upperify").withArgs(str), + yield); + upperized = result[0].to(); }; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) @@ -1523,7 +1514,7 @@ GIVEN( "these test fixture objects" ) subscriber->connect(yield); subscriber->join(Realm(testRealm), yield); subscriber->subscribe(Topic("onEvent"), - unpackedEvent(onEvent), + basicCoroEvent(onEvent), yield); callee->publish(Pub("onEvent").withArgs("Hello"), yield); @@ -1549,21 +1540,13 @@ GIVEN( "these test fixture objects" ) }; auto shout = - [&iosvc, callee](Invocation inv, std::string str) -> Outcome + [callee](Invocation, std::string str, Yield yield) -> Outcome { - // We need a separate yield context here for a blocking - // publish. - boost::asio::spawn(iosvc, [callee, inv, str] - (boost::asio::yield_context yield) - { - std::string upper = str; - std::transform(upper.begin(), upper.end(), - upper.begin(), ::toupper); - callee->publish(Pub("grapevine").withArgs(upper), - yield); - inv.yield(Result({upper})); - }); - return Outcome::deferred(); + std::string upper = str; + std::transform(upper.begin(), upper.end(), + upper.begin(), ::toupper); + callee->publish(Pub("grapevine").withArgs(upper), yield); + return Result({upper}); }; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) @@ -1571,7 +1554,7 @@ GIVEN( "these test fixture objects" ) callee->connect(yield); callee->join(Realm(testRealm), yield); callee->enroll(Procedure("shout"), - unpackedRpc(shout), yield); + unpackedCoroRpc(shout), yield); subscriber->connect(yield); subscriber->join(Realm(testRealm), yield); @@ -1598,34 +1581,27 @@ GIVEN( "these test fixture objects" ) int callCount = 0; Registration reg; - auto oneShot = - [&iosvc, &callCount, ®, callee](Invocation inv) -> Outcome - { - // We need a separate yield context here for a blocking - // unregister. - boost::asio::spawn(iosvc, [&callCount, ®, callee, inv] - (boost::asio::yield_context yield) - { - ++callCount; - callee->unregister(reg, yield); - inv.yield(Result({callCount})); - }); - return Outcome::deferred(); - }; + auto oneShot = [&callCount, ®, callee](Yield yield) + { + // We need a yield context here for a blocking unregister. + ++callCount; + callee->unregister(reg, yield); + }; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { callee->connect(yield); callee->join(Realm(testRealm), yield); - reg = callee->enroll(Procedure("oneShot"), oneShot, yield); + reg = callee->enroll(Procedure("oneShot"), + basicCoroRpc(oneShot), yield); caller->connect(yield); caller->join(Realm(testRealm), yield); - auto result = caller->call(Rpc("oneShot"), yield); + caller->call(Rpc("oneShot"), yield); while (callCount == 0) caller->suspend(yield); - CHECK( result[0] == 1 ); + CHECK( callCount == 1 ); std::error_code ec; caller->call(Rpc("oneShot"), yield, &ec); @@ -1642,18 +1618,14 @@ GIVEN( "these test fixture objects" ) { std::string upperized; - auto onTalk = [&iosvc, session1](Event, std::string str) + auto onTalk = [session1](std::string str, Yield yield) { // We need a separate yield context here for a blocking // publish. - boost::asio::spawn(iosvc, [session1, str] - (boost::asio::yield_context yield) - { - std::string upper = str; - std::transform(upper.begin(), upper.end(), - upper.begin(), ::toupper); - session1->publish(Pub("onShout").withArgs(upper), yield); - }); + std::string upper = str; + std::transform(upper.begin(), upper.end(), + upper.begin(), ::toupper); + session1->publish(Pub("onShout").withArgs(upper), yield); }; auto onShout = [&upperized](Event, std::string str) @@ -1666,7 +1638,7 @@ GIVEN( "these test fixture objects" ) session1->connect(yield); session1->join(Realm(testRealm), yield); session1->subscribe(Topic("onTalk"), - unpackedEvent(onTalk), yield); + basicCoroEvent(onTalk), yield); session2->connect(yield); session2->join(Realm(testRealm), yield); @@ -1692,16 +1664,11 @@ GIVEN( "these test fixture objects" ) int eventCount = 0; Subscription sub; - auto onEvent = [&iosvc, &eventCount, &sub, subscriber](Event) + auto onEvent = [&eventCount, &sub, subscriber](Event, Yield yield) { - // We need a separate yield context here for a blocking - // unsubscribe. - boost::asio::spawn(iosvc, [&eventCount, &sub, subscriber] - (boost::asio::yield_context yield) - { - ++eventCount; - subscriber->unsubscribe(sub, yield); - }); + // We need a yield context here for a blocking unsubscribe. + ++eventCount; + subscriber->unsubscribe(sub, yield); }; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) @@ -1711,7 +1678,9 @@ GIVEN( "these test fixture objects" ) subscriber->connect(yield); subscriber->join(Realm(testRealm), yield); - sub = subscriber->subscribe(Topic("onEvent"), onEvent, yield); + sub = subscriber->subscribe(Topic("onEvent"), + unpackedCoroEvent(onEvent), + yield); // Dummy RPC used to end polling int rpcCount = 0; @@ -1758,7 +1727,7 @@ GIVEN( "an IO service, a valid TCP connector, and an invalid connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(badCnct); + auto session = CoroSession<>::create(iosvc, badCnct); bool throws = false; try { @@ -1786,7 +1755,7 @@ GIVEN( "an IO service, a valid TCP connector, and an invalid connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto s = CoroSession<>::create(connectors); + auto s = CoroSession<>::create(iosvc, connectors); { // Connect @@ -1849,7 +1818,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1874,7 +1843,7 @@ GIVEN( "an IO service and a TCP connector" ) { std::error_code ec; int callCount = 0; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1923,7 +1892,7 @@ GIVEN( "an IO service and a TCP connector" ) { std::error_code ec; int callCount = 0; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -1972,7 +1941,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { std::error_code ec; - RpcFixture f(cnct); + RpcFixture f(iosvc, cnct); f.join(yield); f.enroll(yield); @@ -2001,7 +1970,8 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { PublicationId pid = 0; - PubSubFixture f(cnct); + PubSubFixture f(iosvc, cnct); + f.subscriber->setWarningHandler( [](std::string){} ); f.join(yield); f.subscribe(yield); @@ -2132,7 +2102,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); session->connect(yield); bool throws = false; @@ -2169,7 +2139,8 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "constructing a session with an empty connector list" ) { - CHECK_THROWS_AS( Session::create(ConnectorList{}), error::Logic ); + CHECK_THROWS_AS( Session::create(iosvc, ConnectorList{}), + error::Logic ); } WHEN( "using invalid operations while disconnected" ) @@ -2177,7 +2148,7 @@ GIVEN( "an IO service and a TCP connector" ) boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { std::error_code ec; - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); REQUIRE( session->state() == SessionState::disconnected ); checkInvalidJoin(session, yield); checkInvalidLeave(session, yield); @@ -2189,7 +2160,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "using invalid operations while connecting" ) { - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); session->connect( [](AsyncResult){} ); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) @@ -2210,7 +2181,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(invalidTcp(iosvc)); + auto session = CoroSession<>::create(iosvc, invalidTcp(iosvc)); CHECK_THROWS( session->connect(yield) ); REQUIRE( session->state() == SessionState::failed ); checkInvalidJoin(session, yield); @@ -2225,7 +2196,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); session->connect(yield); REQUIRE( session->state() == SessionState::closed ); checkInvalidConnect(session, yield); @@ -2238,7 +2209,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "using invalid operations while establishing" ) { - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { session->connect(yield); @@ -2265,7 +2236,7 @@ GIVEN( "an IO service and a TCP connector" ) { boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); session->connect(yield); session->join(Realm(testRealm), yield); REQUIRE( session->state() == SessionState::established ); @@ -2278,7 +2249,7 @@ GIVEN( "an IO service and a TCP connector" ) WHEN( "using invalid operations while shutting down" ) { - auto session = CoroSession<>::create(cnct); + auto session = CoroSession<>::create(iosvc, cnct); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { session->connect(yield); @@ -2507,7 +2478,7 @@ GIVEN( "an IO service and a TCP connector" ) bool published = false; boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { - auto s = CoroSession<>::create(cnct); + auto s = CoroSession<>::create(iosvc, cnct); s->connect(yield); s->join(Realm(testRealm), yield); s->publish(Pub("topic"), diff --git a/test/wamptestadvanced.cpp b/test/wamptestadvanced.cpp index 830df1fc..8b1672d9 100644 --- a/test/wamptestadvanced.cpp +++ b/test/wamptestadvanced.cpp @@ -23,20 +23,16 @@ const short testPort = 12345; Connector::Ptr tcp(AsioService& iosvc) { -#ifdef CPPWAMP_USE_LEGACY_CONNECTORS - return legacyConnector(iosvc, TcpHost("localhost", testPort)); -#else return connector(iosvc, TcpHost("localhost", testPort)); -#endif } //------------------------------------------------------------------------------ struct RpcFixture { template - RpcFixture(TConnector cnct) - : caller(CoroSession<>::create(cnct)), - callee(CoroSession<>::create(cnct)) + RpcFixture(AsioService& iosvc, TConnector cnct) + : caller(CoroSession<>::create(iosvc, cnct)), + callee(CoroSession<>::create(iosvc, cnct)) {} void join(boost::asio::yield_context yield) @@ -63,9 +59,9 @@ struct RpcFixture struct PubSubFixture { template - PubSubFixture(TConnector cnct) - : publisher(CoroSession<>::create(cnct)), - subscriber(CoroSession<>::create(cnct)) + PubSubFixture(AsioService& iosvc, TConnector cnct) + : publisher(CoroSession<>::create(iosvc, cnct)), + subscriber(CoroSession<>::create(iosvc, cnct)) {} void join(boost::asio::yield_context yield) @@ -98,7 +94,7 @@ SCENARIO( "WAMP RPC advanced features", "[WAMP]" ) GIVEN( "a caller and a callee" ) { AsioService iosvc; - RpcFixture f(tcp(iosvc)); + RpcFixture f(iosvc, tcp(iosvc)); WHEN( "using caller identification" ) { @@ -175,7 +171,7 @@ SCENARIO( "WAMP pub/sub advanced features", "[WAMP]" ) GIVEN( "a publisher and a subscriber" ) { AsioService iosvc; - PubSubFixture f(tcp(iosvc)); + PubSubFixture f(iosvc, tcp(iosvc)); WHEN( "using publisher identification" ) { @@ -291,7 +287,7 @@ GIVEN( "a publisher and a subscriber" ) WHEN( "using subscriber black/white listing" ) { - auto subscriber2 = CoroSession<>::create(tcp(iosvc)); + auto subscriber2 = CoroSession<>::create(iosvc, tcp(iosvc)); boost::asio::spawn(iosvc, [&](boost::asio::yield_context yield) { diff --git a/toolchain-arm-linux-gnueabihf.cmake b/toolchain-arm-linux-gnueabihf.cmake new file mode 100644 index 00000000..0c5a0987 --- /dev/null +++ b/toolchain-arm-linux-gnueabihf.cmake @@ -0,0 +1,18 @@ +# this one is important +SET(CMAKE_SYSTEM_NAME Linux) +#this one not so much +SET(CMAKE_SYSTEM_VERSION 1) + +# specify the cross compiler +SET(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc) +SET(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++) + +# where is the target environment +SET(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf) + +# search for programs in the build host directories +SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +# for libraries and headers in the target directories +SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +