diff --git a/CMakeLists.txt b/CMakeLists.txt index ffda29459..4d8a53ea9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,29 +2,33 @@ project( AppBase ) cmake_minimum_required( VERSION 2.8.12 ) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules") + +include( InstallDirectoryPermissions ) + file(GLOB HEADERS "include/appbase/*.hpp") set(CMAKE_EXPORT_COMPILE_COMMANDS "ON") -SET(BOOST_COMPONENTS) -LIST(APPEND BOOST_COMPONENTS thread +set(BOOST_COMPONENTS) +list(APPEND BOOST_COMPONENTS thread date_time - system filesystem + system chrono program_options unit_test_framework locale) -FIND_PACKAGE(Boost 1.60 REQUIRED COMPONENTS ${BOOST_COMPONENTS}) +find_package(Boost 1.60 REQUIRED COMPONENTS ${BOOST_COMPONENTS}) set( Boost_USE_STATIC_LIBS ON CACHE STRING "ON or OFF" ) if( APPLE ) # Apple Specific Options Here - message( STATUS "Configuring ChainBase on OS X" ) + message( STATUS "Configuring AppBase on OS X" ) set( CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -std=c++14 -stdlib=libc++ -Wall -Wno-conversion -Wno-deprecated-declarations" ) else( APPLE ) # Linux Specific Options Here - message( STATUS "Configuring ChainBase on Linux" ) + message( STATUS "Configuring AppBase on Linux" ) set( CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -std=c++14 -Wall" ) set( rt_library rt ) set( pthread_library pthread) @@ -40,6 +44,7 @@ endif() add_library( appbase application.cpp + ${HEADERS} ) target_link_libraries( appbase ${Boost_LIBRARIES}) @@ -47,13 +52,18 @@ target_link_libraries( appbase ${Boost_LIBRARIES}) target_include_directories( appbase PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ${Boost_INCLUDE_DIR}) -INSTALL( TARGETS +set_target_properties( appbase PROPERTIES PUBLIC_HEADER "${HEADERS}" ) + +set(CPACK_PACKAGING_INSTALL_PREFIX /) + +install( TARGETS appbase - RUNTIME DESTINATION bin - LIBRARY DESTINATION lib - ARCHIVE DESTINATION lib + RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR} + PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/appbase ) -INSTALL( FILES ${HEADERS} DESTINATION "include/appbase" ) +install_directory_permissions( DIRECTORY ${CMAKE_INSTALL_FULL_INCLUDEDIR}/appbase ) add_subdirectory( examples ) diff --git a/CMakeModules/InstallDirectoryPermissions.cmake b/CMakeModules/InstallDirectoryPermissions.cmake new file mode 100644 index 000000000..b13a80692 --- /dev/null +++ b/CMakeModules/InstallDirectoryPermissions.cmake @@ -0,0 +1,14 @@ +# Fix directory permissions after installation of header files (primarily). +macro(install_directory_permissions) + cmake_parse_arguments(ARG "" "DIRECTORY" "" ${ARGN}) + set(dir ${ARG_DIRECTORY}) + install(DIRECTORY DESTINATION ${dir} + DIRECTORY_PERMISSIONS OWNER_READ + OWNER_WRITE + OWNER_EXECUTE + GROUP_READ + GROUP_EXECUTE + WORLD_READ + WORLD_EXECUTE + ) +endmacro(install_directory_permissions) diff --git a/README.md b/README.md index 1d5b02ed7..e8730d17e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ plugins are configured, initialized, started, and shutdown in the proper order. ## Key Features - Dynamically Specify Plugins to Load -- Automaticly Load Dependent Plugins in Order +- Automatically Load Dependent Plugins in Order - Plugins can specify commandline arguments and configuration file options - Program gracefully exits from SIGINT and SIGTERM - Minimal Dependencies (Boost 1.60, c++14) @@ -87,7 +87,7 @@ exited cleanly ### Boost ASIO AppBase maintains a singleton `application` instance which can be accessed via `appbase::app()`. This -application owns a `boost::asio::io_service` which starts running when appbase::exec() is called. If +application owns a `boost::asio::io_service` which starts running when `appbase::exec()` is called. If a plugin needs to perform IO or other asynchronous operations then it should dispatch it via `app().get_io_service().post( lambda )`. @@ -105,6 +105,7 @@ To trigger a graceful exit call `appbase::app().quit()` or send SIGTERM or SIGIN To compile boost with c++14 use: - ./b2 ... cxxflags="-std=c++0x -stdlib=libc++" linkflags="-stdlib=libc++" ... - +``` +./b2 ... cxxflags="-std=c++0x -stdlib=libc++" linkflags="-stdlib=libc++" ... +``` diff --git a/application.cpp b/application.cpp index 3931ad84b..a32d85047 100644 --- a/application.cpp +++ b/application.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -22,7 +23,11 @@ class application_impl { options_description _app_options; options_description _cfg_options; - bfs::path _data_dir; + bfs::path _data_dir{"data-dir"}; + bfs::path _config_dir{"config-dir"}; + bfs::path _logging_conf{"logging.json"}; + + uint64_t _version; }; application::application() @@ -32,6 +37,36 @@ application::application() application::~application() { } +void application::set_version(uint64_t version) { + my->_version = version; +} + +uint64_t application::version() const { + return my->_version; +} + +void application::set_default_data_dir(const bfs::path& data_dir) { + my->_data_dir = data_dir; +} + +void application::set_default_config_dir(const bfs::path& config_dir) { + my->_config_dir = config_dir; +} + +bfs::path application::get_logging_conf() const { + return my->_logging_conf; +} + +void application::startup() { + try { + for (auto plugin : initialized_plugins) + plugin->startup(); + } catch(...) { + shutdown(); + throw; + } +} + application& application::instance() { static application _app; return _app; @@ -61,44 +96,75 @@ void application::set_program_options() app_cli_opts.add_options() ("help,h", "Print this help message and exit.") ("version,v", "Print version information.") - ("data-dir,d", bpo::value()->default_value( "data-dir" ), "Directory containing configuration file config.ini") - ("config,c", bpo::value()->default_value( "config.ini" ), "Configuration file name relative to data-dir"); + ("print-default-config", "Print default configuration template") + ("data-dir,d", bpo::value(), "Directory containing program runtime data") + ("config-dir", bpo::value(), "Directory containing configuration files such as config.ini") + ("config,c", bpo::value()->default_value( "config.ini" ), "Configuration file name relative to config-dir") + ("logconf,l", bpo::value()->default_value( "logging.json" ), "Logging configuration file name/path for library users"); my->_cfg_options.add(app_cfg_opts); my->_app_options.add(app_cfg_opts); my->_app_options.add(app_cli_opts); } - -bool application::initialize( int argc, char** argv ) -{ +bool application::initialize_impl(int argc, char** argv, vector autostart_plugins) { set_program_options(); bpo::variables_map options; bpo::store(bpo::parse_command_line(argc, argv, my->_app_options), options); if( options.count( "help" ) ) { - cout << my->_app_options << "\n"; + cout << my->_app_options << std::endl; return false; } - bfs::path data_dir = "data-dir"; - if( options.count("data-dir") ) - { - data_dir = options["data-dir"].as(); + if( options.count( "version" ) ) { + cout << my->_version << std::endl; + return false; + } + + if( options.count( "print-default-config" ) ) { + print_default_config(cout); + return false; + } + + if( options.count( "data-dir" ) ) { + // Workaround for 10+ year old Boost defect + // See https://svn.boost.org/trac10/ticket/8535 + // Should be .as() but paths with escaped spaces break bpo e.g. + // std::exception::what: the argument ('/path/with/white\ space') for option '--data-dir' is invalid + auto workaround = options["data-dir"].as(); + bfs::path data_dir = workaround; if( data_dir.is_relative() ) data_dir = bfs::current_path() / data_dir; + my->_data_dir = data_dir; } - my->_data_dir = data_dir; - bfs::path config_file_name = data_dir / "config.ini"; - if( options.count( "config" ) ) { - auto config_file_name = options["config"].as(); - if( config_file_name.is_relative() ) - config_file_name = data_dir / config_file_name; + if( options.count( "config-dir" ) ) { + auto workaround = options["config-dir"].as(); + bfs::path config_dir = workaround; + if( config_dir.is_relative() ) + config_dir = bfs::current_path() / config_dir; + my->_config_dir = config_dir; } + auto workaround = options["logconf"].as(); + bfs::path logconf = workaround; + if( logconf.is_relative() ) + logconf = my->_config_dir / logconf; + my->_logging_conf = logconf; + + workaround = options["config"].as(); + bfs::path config_file_name = workaround; + if( config_file_name.is_relative() ) + config_file_name = my->_config_dir / config_file_name; + if(!bfs::exists(config_file_name)) { + if(config_file_name.compare(my->_config_dir / "config.ini") != 0) + { + cout << "Config file " << config_file_name << " missing." << std::endl; + return false; + } write_default_config(config_file_name); } @@ -116,18 +182,15 @@ bool application::initialize( int argc, char** argv ) get_plugin(name).initialize(options); } } + for (auto plugin : autostart_plugins) + if (plugin != nullptr && plugin->get_state() == abstract_plugin::registered) + plugin->initialize(options); bpo::notify(options); return true; } -void application::startup() -{ - for(auto plug : initialized_plugins) - plug->startup(); -} - void application::shutdown() { for(auto ritr = running_plugins.rbegin(); ritr != running_plugins.rend(); ++ritr) { @@ -140,6 +203,7 @@ void application::shutdown() { running_plugins.clear(); initialized_plugins.clear(); plugins.clear(); + io_serv.reset(); } void application::quit() { @@ -147,6 +211,7 @@ void application::quit() { } void application::exec() { + bool was_bad_alloc = false; std::shared_ptr sigint_set(new boost::asio::signal_set(*io_serv, SIGINT)); sigint_set->async_wait([sigint_set,this](const boost::system::error_code& err, int num) { quit(); @@ -159,9 +224,19 @@ void application::exec() { sigterm_set->cancel(); }); + std::shared_ptr sigfail_set(new boost::asio::signal_set(*io_serv, SIGUSR1)); + sigfail_set->async_wait([sigfail_set,this,&was_bad_alloc](const boost::system::error_code& err, int num) { + quit(); + sigfail_set->cancel(); + was_bad_alloc = true; + }); + io_serv->run(); shutdown(); /// perform synchronous shutdown + + if (was_bad_alloc) + throw boost::interprocess::bad_alloc(); } void application::write_default_config(const bfs::path& cfg_file) { @@ -169,28 +244,47 @@ void application::write_default_config(const bfs::path& cfg_file) { bfs::create_directories(cfg_file.parent_path()); std::ofstream out_cfg( bfs::path(cfg_file).make_preferred().string()); + print_default_config(out_cfg); + out_cfg.close(); +} + +void application::print_default_config(std::ostream& os) { + std::map option_to_plug; + for(auto& plug : plugins) { + boost::program_options::options_description plugin_cli_opts; + boost::program_options::options_description plugin_cfg_opts; + plug.second->set_program_options(plugin_cli_opts, plugin_cfg_opts); + + for(const boost::shared_ptr& opt : plugin_cfg_opts.options()) + option_to_plug[opt->long_name()] = plug.second->name(); + } + for(const boost::shared_ptr od : my->_cfg_options.options()) { - if(!od->description().empty()) - out_cfg << "# " << od->description() << "\n"; + if(!od->description().empty()) { + os << "# " << od->description(); + std::map::iterator it; + if((it = option_to_plug.find(od->long_name())) != option_to_plug.end()) + os << " (" << it->second << ")"; + os << std::endl; + } boost::any store; if(!od->semantic()->apply_default(store)) - out_cfg << "# " << od->long_name() << " = \n"; + os << "# " << od->long_name() << " = " << std::endl; else { auto example = od->format_parameter(); if(example.empty()) // This is a boolean switch - out_cfg << od->long_name() << " = " << "false\n"; + os << od->long_name() << " = " << "false" << std::endl; else { // The string is formatted "arg (=)" example.erase(0, 6); example.erase(example.length()-1); - out_cfg << od->long_name() << " = " << example << "\n"; + os << od->long_name() << " = " << example << std::endl; } } - out_cfg << "\n"; + os << std::endl; } - out_cfg.close(); } abstract_plugin* application::find_plugin(const string& name)const @@ -209,8 +303,12 @@ abstract_plugin& application::get_plugin(const string& name)const { return *ptr; } -bfs::path application::data_dir()const { +bfs::path application::data_dir() const { return my->_data_dir; } +bfs::path application::config_dir() const { + return my->_config_dir; +} + } /// namespace appbase diff --git a/include/appbase/application.hpp b/include/appbase/application.hpp index 95bd5c33c..7ec19fae3 100644 --- a/include/appbase/application.hpp +++ b/include/appbase/application.hpp @@ -1,8 +1,10 @@ #pragma once #include +#include +#include #include #include -#include +#include namespace appbase { namespace bpo = boost::program_options; @@ -13,22 +15,66 @@ namespace appbase { public: ~application(); + + /** @brief Set version + * + * @param version Version output with -v/--version + */ + void set_version(uint64_t version); + /** @brief Get version + * + * @return Version output with -v/--version + */ + uint64_t version() const; + /** @brief Set default data directory + * + * @param data_dir Default data directory to use if not specified + * on the command line. + */ + void set_default_data_dir(const bfs::path& data_dir = "data-dir"); + /** @brief Get data directory + * + * @return Data directory, possibly from command line + */ + bfs::path data_dir() const; + /** @brief Set default config directory + * + * @param config_dir Default configuration directory to use if not + * specified on the command line. + */ + void set_default_config_dir(const bfs::path& config_dir = "etc"); + /** @brief Get config directory + * + * @return Config directory, possibly from command line + */ + bfs::path config_dir() const; + /** @brief Get logging configuration path. + * + * @return Logging configuration location from command line + */ + bfs::path get_logging_conf() const; /** - * Looks for the --plugin commandline / config option and calls initialize on those plugins + * @brief Looks for the --plugin commandline / config option and calls initialize on those plugins * - * @return true if the application and plugins were initialized, false or exception on error + * @tparam Plugin List of plugins to initalize even if not mentioned by configuration. For plugins started by + * configuration settings or dependency resolution, this template has no effect. + * @return true if the application and plugins were initialized, false or exception on error */ - bool initialize(int argc, char** argv); - void startup(); - void shutdown(); + template + bool initialize(int argc, char** argv) { + return initialize_impl(argc, argv, {find_plugin()...}); + } + + void startup(); + void shutdown(); /** * Wait until quit(), SIGINT or SIGTERM and then shutdown */ - void exec(); - void quit(); + void exec(); + void quit(); - static application& instance(); + static application& instance(); abstract_plugin* find_plugin(const string& name)const; abstract_plugin& get_plugin(const string& name)const; @@ -57,14 +103,55 @@ namespace appbase { return *ptr; } - bfs::path data_dir()const; + /** + * Fetch a reference to the method declared by the passed in type. This will construct the method + * on first access. This allows loose and deferred binding between plugins + * + * @tparam MethodDecl - @ref appbase::method_decl + * @return reference to the method described by the declaration + */ + template + auto get_method() -> std::enable_if_t::value, typename MethodDecl::method_type&> + { + using method_type = typename MethodDecl::method_type; + auto key = std::type_index(typeid(MethodDecl)); + auto itr = methods.find(key); + if(itr != methods.end()) { + return *method_type::get_method(itr->second); + } else { + methods.emplace(std::make_pair(key, method_type::make_unique())); + return *method_type::get_method(methods.at(key)); + } + } + /** + * Fetch a reference to the channel declared by the passed in type. This will construct the channel + * on first access. This allows loose and deferred binding between plugins + * + * @tparam ChannelDecl - @ref appbase::channel_decl + * @return reference to the channel described by the declaration + */ + template + auto get_channel() -> std::enable_if_t::value, typename ChannelDecl::channel_type&> + { + using channel_type = typename ChannelDecl::channel_type; + auto key = std::type_index(typeid(ChannelDecl)); + auto itr = channels.find(key); + if(itr != channels.end()) { + return *channel_type::get_channel(itr->second); + } else { + channels.emplace(std::make_pair(key, channel_type::make_unique(io_serv))); + return *channel_type::get_channel(channels.at(key)); + } + } boost::asio::io_service& get_io_service() { return *io_serv; } protected: template friend class plugin; + bool initialize_impl(int argc, char** argv, vector autostart_plugins); + /** these notifications get called from the plugin when their state changes so that * the application can call shutdown in the reverse order. */ @@ -74,14 +161,19 @@ namespace appbase { ///@} private: - application(); ///< private because application is a singlton that should be accessed via instance() + application(); ///< private because application is a singleton that should be accessed via instance() map> plugins; ///< all registered plugins vector initialized_plugins; ///< stored in the order they were started running vector running_plugins; ///< stored in the order they were started running + + map methods; + map channels; + std::shared_ptr io_serv; void set_program_options(); void write_default_config(const bfs::path& cfg_file); + void print_default_config(std::ostream& os); std::unique_ptr my; }; diff --git a/include/appbase/channel.hpp b/include/appbase/channel.hpp new file mode 100644 index 000000000..f8a710875 --- /dev/null +++ b/include/appbase/channel.hpp @@ -0,0 +1,207 @@ +#pragma once + +//clashes with BOOST PP and Some Applications +#pragma push_macro("N") +#undef N + +#include +#include +#include + +namespace appbase { + + using erased_channel_ptr = std::unique_ptr; + + /** + * A basic DispatchPolicy that will catch and drop any exceptions thrown + * during the dispatch of messages on a channel + */ + struct drop_exceptions { + drop_exceptions() = default; + using result_type = void; + + template + result_type operator()(InputIterator first, InputIterator last) { + while (first != last) { + try { + *first; + } catch (...) { + // drop + } + ++first; + } + } + }; + + /** + * A channel is a loosely bound asynchronous data pub/sub concept. + * + * This removes the need to tightly couple different plugins in the application for the use-case of + * sending data around + * + * Data passed to a channel is *copied*, consider using a shared_ptr if the use-case allows it + * + * @tparam Data - the type of data to publish + */ + template + class channel final { + public: + using ios_ptr_type = std::shared_ptr; + + /** + * Type that represents an active subscription to a channel allowing + * for ownership via RAII and also explicit unsubscribe actions + */ + class handle { + public: + ~handle() { + unsubscribe(); + } + + /** + * Explicitly unsubcribe from channel before the lifetime + * of this object expires + */ + void unsubscribe() { + if (_handle.connected()) { + _handle.disconnect(); + } + } + + // This handle can be constructed and moved + handle() = default; + handle(handle&&) = default; + handle& operator= (handle&& rhs) = default; + + // dont allow copying since this protects the resource + handle(const handle& ) = delete; + handle& operator= (const handle& ) = delete; + + private: + using handle_type = boost::signals2::connection; + handle_type _handle; + + /** + * Construct a handle from an internal represenation of a handle + * In this case a boost::signals2::connection + * + * @param _handle - the boost::signals2::connection to wrap + */ + handle(handle_type&& _handle) + :_handle(std::move(_handle)) + {} + + friend class channel; + }; + + /** + * Publish data to a channel. This data is *copied* on publish. + * @param data - the data to publish + */ + void publish(const Data& data) { + if (has_subscribers()) { + // this will copy data into the lambda + ios_ptr->post([this, data]() { + _signal(data); + }); + } + } + + /** + * subscribe to data on a channel + * @tparam Callback the type of the callback (functor|lambda) + * @param cb the callback + * @return handle to the subscription + */ + template + handle subscribe(Callback cb) { + return handle(_signal.connect(cb)); + } + + /** + * set the dispatcher according to the DispatchPolicy + * this can be used to set a stateful dispatcher + * + * This method is only available when the DispatchPolicy is copy constructible due to implementation details + * + * @param policy - the DispatchPolicy to copy + */ + auto set_dispatcher(const DispatchPolicy& policy ) -> std::enable_if_t::value,void> + { + _signal.set_combiner(policy); + } + + /** + * Returns whether or not there are subscribers + */ + bool has_subscribers() { + auto connections = _signal.num_slots(); + return connections > 0; + } + + private: + explicit channel(const ios_ptr_type& ios_ptr) + :ios_ptr(ios_ptr) + { + } + + virtual ~channel() = default; + + /** + * Proper deleter for type-erased channel + * note: no type checking is performed at this level + * + * @param erased_channel_ptr + */ + static void deleter(void* erased_channel_ptr) { + auto ptr = reinterpret_cast(erased_channel_ptr); + delete ptr; + } + + /** + * get the channel back from an erased pointer + * + * @param ptr - the type-erased channel pointer + * @return - the type safe channel pointer + */ + static channel* get_channel(erased_channel_ptr& ptr) { + return reinterpret_cast(ptr.get()); + } + + /** + * Construct a unique_ptr for the type erased method poiner + * @return + */ + static erased_channel_ptr make_unique(const ios_ptr_type& ios_ptr) + { + return erased_channel_ptr(new channel(ios_ptr), &deleter); + } + + ios_ptr_type ios_ptr; + boost::signals2::signal _signal; + + friend class appbase::application; + }; + + /** + * + * @tparam Tag - API specific discriminator used to distinguish between otherwise identical data types + * @tparam Data - the typ of the Data the channel carries + * @tparam DispatchPolicy - The dispatch policy to use for this channel (defaults to @ref drop_exceptions) + */ + template< typename Tag, typename Data, typename DispatchPolicy = drop_exceptions > + struct channel_decl { + using channel_type = channel; + using tag_type = Tag; + }; + + template + std::true_type is_channel_decl_impl(const channel_decl*); + + std::false_type is_channel_decl_impl(...); + + template + using is_channel_decl = decltype(is_channel_decl_impl(std::declval())); +} + +#pragma pop_macro("N") diff --git a/include/appbase/method.hpp b/include/appbase/method.hpp new file mode 100644 index 000000000..b7f2d0373 --- /dev/null +++ b/include/appbase/method.hpp @@ -0,0 +1,318 @@ +#pragma once + +//clashes with BOOST PP and Some Applications +#pragma push_macro("N") +#undef N + +#include +#include + +namespace appbase { + + using erased_method_ptr = std::unique_ptr; + + /** + * Basic DispatchPolicy that will try providers sequentially until one succeeds + * without throwing an exception. that result becomes the result of the method + */ + template + struct first_success_policy; + + template + struct first_success_policy { + using result_type = Ret; + + /** + * Iterate through the providers, calling (dereferencing) each + * if the provider throws, then store then try the next provider + * if none succeed throw an error with the aggregated error descriptions + * + * @tparam InputIterator + * @param first + * @param last + * @return + */ + template + Ret operator()(InputIterator first, InputIterator last) { + std::string err; + while (first != last) { + try { + return *first; // de-referencing the iterator causes the provider to run + } catch (...) { + if (!err.empty()) { + err += "\",\""; + } + + err += boost::current_exception_diagnostic_information(); + } + + ++first; + } + + throw std::length_error(std::string("No Result Available, All providers returned exceptions[") + err + "]"); + } + }; + + template + struct first_success_policy { + using result_type = void; + + /** + * Iterate through the providers, calling (dereferencing) each + * if the provider throws, then store then try the next provider + * if none succeed throw an error with the aggregated error descriptions + * + * @tparam InputIterator + * @param first + * @param last + * @return + */ + template + void operator()(InputIterator first, InputIterator last) { + std::string err; + + while (first != last) { + try { + *first; // de-referencing the iterator causes the provider to run + } catch (...) { + if (!err.empty()) { + err += "\",\""; + } + + err += boost::current_exception_diagnostic_information(); + } + + ++first; + } + + throw std::length_error(std::string("No Result Available, All providers returned exceptions[") + err + "]"); + } + }; + + + /** + * Basic DispatchPolicy that will only call the first provider throwing or returning that providers results + */ + template + struct first_provider_policy; + + template + struct first_provider_policy { + using result_type = Ret; + + /** + * Call the first provider as ordered by registered priority, return its result + * throw its exceptions + * + * @tparam InputIterator + * @param first + * @param + * @return + */ + template + Ret operator()(InputIterator first, InputIterator) { + return *first; + } + }; + + template + struct first_provider_policy { + using result_type = void; + + /** + * Call the first provider as ordered by registered priority, return its result + * throw its exceptions + * + * @tparam InputIterator + * @param first + * @param + * @return + */ + template + void operator()(InputIterator first, InputIterator) { + *first; + } + }; + + namespace impl { + template + class method_caller; + + template + class method_caller { + public: + using signal_type = boost::signals2::signal; + using result_type = Ret; + + method_caller() + {} + + /** + * call operator from boost::signals2 + * + * @throws exception depending on the DispatchPolicy + */ + Ret operator()(Args&&... args) + { + return _signal(std::forward(args)...); + } + + signal_type _signal; + }; + + template + class method_caller { + public: + using signal_type = boost::signals2::signal; + using result_type = void; + + method_caller() + {} + + /** + * call operator from boost::signals2 + * + * @throws exception depending on the DispatchPolicy + */ + void operator()(Args&&... args) + { + _signal(std::forward(args)...); + } + + signal_type _signal; + }; + } + + /** + * A method is a loosely linked application level function. + * Callers can grab a method and call it + * Providers can grab a method and register themselves + * + * This removes the need to tightly couple different plugins in the application. + * + * @tparam FunctionSig - the signature of the method (eg void(int, int)) + * @tparam DispatchPolicy - the policy for dispatching this method + */ + template + class method final : public impl::method_caller { + public: + /** + * Type that represents a registered provider for a method allowing + * for ownership via RAII and also explicit unregistered actions + */ + class handle { + public: + ~handle() { + unregister(); + } + + /** + * Explicitly unregister a provider for this channel + * of this object expires + */ + void unregister() { + if (_handle.connected()) { + _handle.disconnect(); + } + } + + // This handle can be constructed and moved + handle() = default; + handle(handle&&) = default; + handle& operator= (handle&& rhs) = default; + + // dont allow copying since this protects the resource + handle(const handle& ) = delete; + handle& operator= (const handle& ) = delete; + + private: + using handle_type = boost::signals2::connection; + handle_type _handle; + + /** + * Construct a handle from an internal represenation of a handle + * In this case a boost::signals2::connection + * + * @param _handle - the boost::signals2::connection to wrap + */ + handle(handle_type&& _handle) + :_handle(std::move(_handle)) + {} + + friend class method; + }; + + /** + * Register a provider of this method + * + * @tparam T - the type of the provider (functor, lambda) + * @param provider - the provider + * @param priority - the priority of this provider, lower is called before higher + */ + template + handle register_provider(T provider, int priority = 0) { + return handle(this->_signal.connect(priority, provider)); + } + + protected: + method() = default; + virtual ~method() = default; + + /** + * Proper deleter for type-erased method + * note: no type checking is performed at this level + * + * @param erased_method_ptr + */ + static void deleter(void* erased_method_ptr) { + auto ptr = reinterpret_cast(erased_method_ptr); + delete ptr; + } + + /** + * get the method* back from an erased pointer + * + * @param ptr - the type-erased method pointer + * @return - the type safe method pointer + */ + static method* get_method(erased_method_ptr& ptr) { + return reinterpret_cast(ptr.get()); + } + + /** + * Construct a unique_ptr for the type erased method poiner + * @return + */ + static erased_method_ptr make_unique() { + return erased_method_ptr(new method(), &deleter); + } + + friend class appbase::application; + }; + + + /** + * + * @tparam Tag - API specific discriminator used to distinguish between otherwise identical method signatures + * @tparam FunctionSig - the signature of the method + * @tparam DispatchPolicy - dispatch policy that dictates how providers for a method are accessed defaults to @ref first_success_policy + */ + template< typename Tag, typename FunctionSig, template class DispatchPolicy = first_success_policy> + struct method_decl { + using method_type = method>; + using tag_type = Tag; + }; + + template class DispatchPolicy> + std::true_type is_method_decl_impl(const method_decl*); + + std::false_type is_method_decl_impl(...); + + template + using is_method_decl = decltype(is_method_decl_impl(std::declval())); + + +} + +#pragma pop_macro("N") + diff --git a/include/appbase/plugin.hpp b/include/appbase/plugin.hpp index 26f4372e4..bf0857269 100644 --- a/include/appbase/plugin.hpp +++ b/include/appbase/plugin.hpp @@ -29,7 +29,7 @@ namespace appbase { public: enum state { registered, ///< the plugin is constructed but doesn't do anything - initialized, ///< the plugin has initlaized any state required but is idle + initialized, ///< the plugin has initialized any state required but is idle started, ///< the plugin is actively running stopped ///< the plugin is no longer running };