diff --git a/CMakeLists.txt b/CMakeLists.txt index bdc4f924ba4dd5fda3b65643d70be80e52fcb30b..3695cb6d85a68baced219cc0d63dc3ad4d888c7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,6 +134,7 @@ add_executable(catch-tests-boardcore src/tests/catch/test-sensormanager-catch.cpp src/tests/catch/xbee/test-xbee-parser.cpp src/tests/catch/test-modulemanager.cpp + src/tests/catch/test-dependencymanager.cpp src/tests/catch/test-MEA.cpp src/tests/catch/test-airbrakesInterp.cpp src/tests/catch/test-pitot.cpp diff --git a/cmake/boardcore-host.cmake b/cmake/boardcore-host.cmake index a0a685e46cd1549783fdb6626be877a60b50389b..6f22f2b1fde182ffdb17fb4b6470bc51de995bb8 100644 --- a/cmake/boardcore-host.cmake +++ b/cmake/boardcore-host.cmake @@ -57,6 +57,7 @@ set(BOARDCORE_HOST_SRC ${SBS_BASE}/src/shared/utils/TestUtils/TestHelper.cpp ${SBS_BASE}/src/shared/utils/Registry/RegistryFrontend.cpp ${SBS_BASE}/src/shared/utils/Registry/RegistrySerializer.cpp + ${SBS_BASE}/src/shared/utils/DependencyManager/DependencyManager.cpp ) # Create a library specific for host builds diff --git a/cmake/boardcore.cmake b/cmake/boardcore.cmake index a95d56bd1dfa7ab560f5d28dcd60cdad9a249053..309394fcfbd8a65727d0878fc11c102e9f6906cc 100644 --- a/cmake/boardcore.cmake +++ b/cmake/boardcore.cmake @@ -135,6 +135,7 @@ set(BOARDCORE_SRC ${BOARDCORE_PATH}/src/shared/utils/Registry/RegistryFrontend.cpp ${BOARDCORE_PATH}/src/shared/utils/Registry/RegistrySerializer.cpp ${BOARDCORE_PATH}/src/shared/utils/Registry/Backend/FileBackend.cpp + ${BOARDCORE_PATH}/src/shared/utils/DependencyManager/DependencyManager.cpp ) # Creates the Skyward::Boardcore::${BOARD_NAME} library diff --git a/src/shared/utils/DependencyManager/DependencyManager.cpp b/src/shared/utils/DependencyManager/DependencyManager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fe4d3357452d98ded20b57d0de23be2c188db6a8 --- /dev/null +++ b/src/shared/utils/DependencyManager/DependencyManager.cpp @@ -0,0 +1,115 @@ +/* Copyright (c) 2024 Skyward Experimental Rocketry + * Authors: Davide Mor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "DependencyManager.h" + +#include <cxxabi.h> +#include <fmt/format.h> + +using namespace Boardcore; + +// Simple utility function to demangle type infos +std::string type_name_demangled(const std::type_info& info) +{ + char* demangled = + abi::__cxa_demangle(info.name(), nullptr, nullptr, nullptr); + std::string demangled2{demangled}; + std::free(demangled); + + return demangled2; +} + +void DependencyManager::graphviz(std::ostream& os) +{ + os << "digraph {" << std::endl; + + for (auto& module : modules) + { + for (auto& dep : module.second.deps) + { + os << fmt::format(" \"{}({})\" -> \"{}({})\"", module.second.name, + module.second.impl, modules[dep].name, + modules[dep].impl) + << std::endl; + } + } + + os << "}" << std::endl; +} + +bool DependencyManager::inject() +{ + load_success = true; + + for (auto& module : modules) + { + LOG_INFO(logger, "Configuring [{}]...", module.second.name); + DependencyInjector injector{*this, module.second}; + module.second.ptr->inject(injector); + } + + if (load_success) + { + LOG_INFO(logger, "Configuring successful!"); + } + else + { + LOG_ERR(logger, "Failed to inject modules!"); + } + + return load_success; +} + +bool DependencyManager::insertImpl(Injectable* ptr, + const std::type_info& module_info, + const std::type_info& impl_info) +{ + // Early check to see if ptr is nullptr, fail if that's the case + if (ptr == nullptr) + return false; + + auto idx = std::type_index{module_info}; + auto module_name = type_name_demangled(module_info); + auto impl_name = type_name_demangled(impl_info); + + return modules.insert({idx, ModuleInfo{ptr, module_name, impl_name, {}}}) + .second; +} + +Injectable* DependencyInjector::getImpl(const std::type_info& module_info) +{ + auto idx = std::type_index{module_info}; + auto iter = manager.modules.find(idx); + if (iter == manager.modules.end()) + { + manager.load_success = false; + + std::string module_name = type_name_demangled(module_info); + LOG_ERR(logger, "[{}] requires [{}], but the latter is not present", + info.name, module_name); + + return nullptr; + } + + info.deps.push_back(idx); + return iter->second.ptr; +} diff --git a/src/shared/utils/DependencyManager/DependencyManager.h b/src/shared/utils/DependencyManager/DependencyManager.h new file mode 100644 index 0000000000000000000000000000000000000000..4a1826b2574c4c26c57747f12b5768bc904a6349 --- /dev/null +++ b/src/shared/utils/DependencyManager/DependencyManager.h @@ -0,0 +1,276 @@ +/* Copyright (c) 2024 Skyward Experimental Rocketry + * Authors: Davide Mor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include <diagnostic/PrintLogger.h> + +#include <ostream> +#include <string> +#include <typeindex> +#include <typeinfo> +#include <unordered_map> +#include <vector> + +namespace Boardcore +{ + +class DependencyInjector; +class DependencyManager; + +/** + * @brief Interface for an injectable dependency. + */ +class Injectable +{ +public: + virtual ~Injectable() = default; + + /** + * @brief Invoked by the DependencyManager to inject dependencies. + * Override this method to retrieve dependencies from the injector via + * `DependencyInjector::get()`. + * + * @param injector Proxy class used to obtain dependencies. + */ + virtual void inject(DependencyInjector &injector) {} +}; + +/** + * @brief Main DependencyManager class. + * + * This utility is meant to be used as a dependency injector for Skyward OBSW. + * + * Dependencies and dependents should inherit from `Injectable`. Normally though + * you should extend from `InjectableWithDeps` in case of dependencies. + * + * Here's a quick example (for more examples look at + * src/tests/catch/test-dependencymanager.cpp): + * @code{.cpp} + * + * // A simple direct dependency + * class MyDependency1 : public Injectable {}; + * + * // Abstracting direct dependencies with a common interface + * class MyDependency2Iface {}; + * class MyDependency2 : public Injectable, public MyDependency2Iface {}; + * + * // A simple dependant (which can become a dependency itself) + * class MyDependant : public InjectableWithDeps<MyDependency1, + * MyDependency2Iface> {}; + * + * DependencyManager dependency_mgr; + * + * // Initialize the dependencies + * MyDependency1 *dep1 = ; + * MyDependency2Iface *dep2 = new MyDependency2(); + * + * dependency_mgr.insert<MyDependency1>(new MyDependency1()); + * dependency_mgr.insert<MyDependency2Iface>(new MyDependency2()); + * dependency_mgr.insert<MyDependant>(new MyDependant()); + * + * // Inject and resolve all dependencies! + * dependency_mgr.inject(); + * + * // Optionally, print the resulting graph + * dependency_mgr.graphviz(std::cout); + * @endcode + */ +class DependencyManager +{ + friend class DependencyInjector; + +private: + struct ModuleInfo + { + Injectable *ptr; + // Name of the module interface + std::string name; + // Name of the actual concrete implementation of this module interface + std::string impl; + std::vector<std::type_index> deps; + }; + +public: + DependencyManager() {} + + /** + * @brief Insert a new dependency. + * + * @param dependency Injectable to insert in the DependencyManager. + * @returns True if successful, false otherwise. + */ + template <typename T> + [[nodiscard]] bool insert(T *dependency) + { + return insertImpl(dynamic_cast<Injectable *>(dependency), typeid(T), + typeid(*dependency)); + } + + /** + * @brief Generate a gaphviz compatible output showing dependencies. + * Needs to be called after inject. + * + * @param os Output stream to write to. + */ + void graphviz(std::ostream &os); + + /** + * @brief Inject all dependencies into all inserted . + * + * @returns True if successful, false otherwise. + */ + [[nodiscard]] bool inject(); + +private: + [[nodiscard]] bool insertImpl(Injectable *ptr, + const std::type_info &module_info, + const std::type_info &impl_info); + + Boardcore::PrintLogger logger = + Boardcore::Logging::getLogger("DependencyManager"); + + bool load_success = true; + std::unordered_map<std::type_index, ModuleInfo> modules; +}; + +/** + * @brief Proxy class used to obtain dependencies. + */ +class DependencyInjector +{ + friend class DependencyManager; + +private: + DependencyInjector(DependencyManager &manager, + DependencyManager::ModuleInfo &info) + : manager(manager), info(info) + { + } + +public: + /** + * @brief Retrieve a specific dependencies, recording it and tracking + * unsatisfied dependencies. + * + * @returns The requested dependency or nullptr if not found. + */ + template <typename T> + T *get() + { + return dynamic_cast<T *>(getImpl(typeid(T))); + } + +private: + Injectable *getImpl(const std::type_info &module_info); + + Boardcore::PrintLogger logger = + Boardcore::Logging::getLogger("DependencyManager"); + + DependencyManager &manager; + DependencyManager::ModuleInfo &info; +}; + +namespace DependencyManagerDetails +{ + +// Storage implementation +template <typename... Types> +struct Storage +{ + // No-op, base case + void inject(DependencyInjector &injector) {} + + // No-op, dummy get (this should never be reached) + template <typename T> + T *get() + { + return nullptr; + } +}; + +template <typename Type, typename... Types> +struct Storage<Type, Types...> : public Storage<Types...> +{ + using Super = Storage<Types...>; + + // Recursive implementation + Type *item = nullptr; + + void inject(DependencyInjector &injector) + { + item = injector.get<Type>(); + // Call parent function + Super::inject(injector); + } + + template <typename T> + typename std::enable_if_t<std::is_same<T, Type>::value, T *> get() + { + return item; + } + + template <typename T> + typename std::enable_if_t<!std::is_same<T, Type>::value, T *> get() + { + return Super::template get<T>(); + } +}; + +// Find type in list implementation +template <typename T, typename... Types> +struct Contains : std::false_type +{ +}; + +template <typename T, typename Type, typename... Types> +struct Contains<T, Type, Types...> + : std::integral_constant<bool, std::is_same<T, Type>::value || + Contains<T, Types...>::value> +{ +}; + +} // namespace DependencyManagerDetails + +template <typename... Types> +class InjectableWithDeps : public Injectable +{ +public: + virtual void inject(DependencyInjector &injector) override + { + storage.inject(injector); + } + + template <typename T> + T *getModule() + { + static_assert(DependencyManagerDetails::Contains<T, Types...>::value, + "Dependency T is not present in the dependencies"); + + return storage.template get<T>(); + } + +private: + DependencyManagerDetails::Storage<Types...> storage; +}; + +} // namespace Boardcore diff --git a/src/tests/catch/test-dependencymanager.cpp b/src/tests/catch/test-dependencymanager.cpp new file mode 100644 index 0000000000000000000000000000000000000000..863e87f5123f1529c438a5d2e783b12a60ba7866 --- /dev/null +++ b/src/tests/catch/test-dependencymanager.cpp @@ -0,0 +1,165 @@ +/* Copyright (c) 2024 Skyward Experimental Rocketry + * Author: Davide Mor + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include <utils/DependencyManager/DependencyManager.h> + +#include <catch2/catch.hpp> + +using namespace Boardcore; + +namespace Boardcore +{ +class B; + +class A : public Injectable +{ +public: + A() {} + + void bing_a(bool value) { this->value = value; } + + bool bong_a() { return value; } + + void inject(DependencyInjector &getter) { b = getter.get<B>(); } + +private: + B *b = nullptr; + bool value = false; +}; + +class B : public Injectable +{ +public: + B() {} + + void bing_b(bool value) { this->value = value; } + + bool bong_b() { return value; } + + void inject(DependencyInjector &getter) { a = getter.get<A>(); } + +private: + A *a = nullptr; + bool value = false; +}; + +class CIface +{ +public: + virtual void bing_c() = 0; + virtual bool bong_c() = 0; +}; + +class C : public CIface, public InjectableWithDeps<A, B> +{ +public: + void bing_c() override + { + value = getModule<A>()->bong_a() && getModule<B>()->bong_b(); + } + + bool bong_c() override { return value; } + +private: + bool value = false; +}; + +class D : public Injectable +{ +public: + void bing_d() { value = c->bong_c(); } + + bool bong_d() { return value; } + + void inject(DependencyInjector &getter) { c = getter.get<CIface>(); } + +private: + CIface *c = nullptr; + bool value = false; +}; +} // namespace Boardcore + +TEST_CASE("DependencyManager - Circular dependencies") +{ + DependencyManager manager; + + Boardcore::A *a = new Boardcore::A(); + Boardcore::B *b = new Boardcore::B(); + + REQUIRE(manager.insert<Boardcore::A>(a)); + REQUIRE(manager.insert<Boardcore::B>(b)); + REQUIRE(manager.inject()); + + a->bing_a(true); + REQUIRE(a->bong_a()); + + a->bing_a(false); + REQUIRE(!a->bong_a()); + + b->bing_b(true); + REQUIRE(b->bong_b()); + + b->bing_b(false); + REQUIRE(!b->bong_b()); +} + +TEST_CASE("DependencyManager - Virtual Dependencies") +{ + DependencyManager manager; + + Boardcore::A *a = new Boardcore::A(); + Boardcore::B *b = new Boardcore::B(); + Boardcore::C *c = new Boardcore::C(); + Boardcore::D *d = new Boardcore::D(); + + REQUIRE(manager.insert<Boardcore::A>(a)); + REQUIRE(manager.insert<Boardcore::B>(b)); + REQUIRE(manager.insert<Boardcore::CIface>(c)); + REQUIRE(manager.insert<Boardcore::D>(d)); + REQUIRE(manager.inject()); + + a->bing_a(false); + b->bing_b(false); + + c->bing_c(); + REQUIRE(!c->bong_c()); + d->bing_d(); + REQUIRE(!d->bong_d()); + + a->bing_a(true); + b->bing_b(true); + + c->bing_c(); + REQUIRE(c->bong_c()); + d->bing_d(); + REQUIRE(d->bong_d()); +} + +TEST_CASE("DependencyManager - Inject fail") +{ + DependencyManager manager; + + Boardcore::A *a = new Boardcore::A(); + + REQUIRE(manager.insert<Boardcore::A>(a)); + REQUIRE_FALSE(manager.inject()); +}