-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add services un/provision and access.
- Move test framework into exe target. - Add CLI option to run tests instead of the game. Unit tests should ideally be in a separate executable, but that would require refactoring all the code into a library that both the game exe and unit tests exe would link to. This monolithic approach keeps things simple at the cost of game binary bloat.
- Loading branch information
Showing
9 changed files
with
244 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
#pragma once | ||
#include <fmt/format.h> | ||
#include <stdexcept> | ||
|
||
class FatalError : public std::runtime_error { | ||
public: | ||
template <typename... Args> | ||
explicit FatalError(fmt::format_string<Args...> fmt, Args&&... args) | ||
: std::runtime_error(fmt::format(fmt, std::forward<Args>(args)...)) {} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
#include <src/services/services.hpp> | ||
#include <mutex> | ||
#include <unordered_map> | ||
|
||
namespace { | ||
struct { | ||
std::unordered_map<std::type_index, void*> services{}; | ||
std::mutex mutex{}; | ||
} g_data{}; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||
} // namespace | ||
|
||
namespace services { | ||
void detail::provide(std::type_index const index, void* obj) { | ||
auto lock = std::scoped_lock{g_data.mutex}; | ||
if (g_data.services.contains(index)) { throw FatalError{"Duplicate service instance"}; } | ||
g_data.services.insert_or_assign(index, obj); | ||
} | ||
|
||
void detail::unprovide(std::type_index const index) { | ||
auto lock = std::scoped_lock{g_data.mutex}; | ||
g_data.services.erase(index); | ||
} | ||
|
||
auto detail::find(std::type_index const index) -> void* { | ||
auto lock = std::scoped_lock{g_data.mutex}; | ||
if (auto const it = g_data.services.find(index); it != g_data.services.end()) { return it->second; } | ||
return {}; | ||
} | ||
} // namespace services | ||
|
||
void services::unprovide_all() { | ||
auto lock = std::scoped_lock{g_data.mutex}; | ||
g_data.services.clear(); | ||
} | ||
|
||
auto services::get_count() -> std::size_t { | ||
auto lock = std::scoped_lock{g_data.mutex}; | ||
return g_data.services.size(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
#pragma once | ||
#include <bave/core/not_null.hpp> | ||
#include <bave/core/ptr.hpp> | ||
#include <src/fatal_error.hpp> | ||
#include <typeindex> | ||
|
||
/// \brief Concept for a serviceable type: it must be pinned since its address is stored. | ||
template <typename Type> | ||
concept ServiceT = !std::is_move_assignable_v<Type> && !std::is_copy_assignable_v<Type>; | ||
|
||
/// \brief Services are effectively globals, since any code anywhere can poke at them. | ||
/// The design employed here minimizes the negative effects of globals via: | ||
/// 1. Type safe service access / provision / unprovision. | ||
/// 2. No ownership of service objects. | ||
/// 3. Consolidated storage of all such globals (as opposed to singletons). | ||
/// 4. Batch removal of all provided services. | ||
/// | ||
/// Point 2 means that a service instance must actually be (uniquely) owned elsewhere. | ||
/// This provides deterministic lifetime of all services, | ||
/// as opposed to global storage, which pushes lifetimes beyond main's scope, and | ||
/// makes the initialization/destruction order unspecified wrt each other. | ||
namespace services { | ||
/// \brief Internal usage, do not use anything in detail directly. | ||
namespace detail { | ||
void provide(std::type_index index, void* obj); | ||
void unprovide(std::type_index index); | ||
[[nodiscard]] auto find(std::type_index index) -> void*; | ||
} // namespace detail | ||
|
||
/// \brief Provide a service by registering its address. | ||
/// \param obj Address of service object. | ||
/// \pre Type must not already be provided. | ||
template <ServiceT Type> | ||
void provide(bave::NotNull<Type*> obj) { | ||
detail::provide(std::type_index{typeid(Type)}, obj.get()); | ||
} | ||
|
||
/// \brief Unprovide a service. Safe to call even if service hasn't been provided. | ||
template <ServiceT Type> | ||
void unprovide() { | ||
detail::unprovide(std::type_index{typeid(Type)}); | ||
} | ||
|
||
/// \brief Unregister all provided services. | ||
void unprovide_all(); | ||
|
||
/// \brief Obtain the count of provided services. | ||
[[nodiscard]] auto get_count() -> std::size_t; | ||
|
||
/// \brief Locate a service if provided. | ||
/// \returns Pointer to service instance if provided, nullptr otherwise. | ||
template <ServiceT Type> | ||
[[nodiscard]] auto find() -> bave::Ptr<Type> { | ||
return static_cast<Type*>(detail::find(std::type_index{typeid(Type)})); | ||
} | ||
|
||
/// \brief Check if a service type has been provided. | ||
/// \returns true if provided. | ||
template <ServiceT Type> | ||
[[nodiscard]] auto contains() -> bool { | ||
return find<Type>() != nullptr; | ||
} | ||
|
||
/// \brief Locate a provided service. | ||
/// \returns Reference to provided service instance. | ||
/// \pre Type must have been provided. | ||
template <ServiceT Type> | ||
[[nodiscard]] auto get() -> Type& { | ||
auto* ret = find<Type>(); | ||
if (ret == nullptr) { throw FatalError{"Requested service not present"}; } | ||
return *ret; | ||
} | ||
} // namespace services | ||
|
||
/// \brief CRTP base class for scoped (RAII) service types. | ||
/// | ||
/// Usage: | ||
/// struct Foo : CrtpService<Foo> { /*...*/ }; | ||
/// Creating a Foo instance will auto-provide Foo service. | ||
/// It will be unprovided on destruction. | ||
template <ServiceT Type> | ||
class CrtpService { | ||
public: | ||
CrtpService(CrtpService&&) = delete; | ||
CrtpService(CrtpService const&) = delete; | ||
auto operator=(CrtpService&&) -> CrtpService& = delete; | ||
auto operator=(CrtpService const&) -> CrtpService& = delete; | ||
|
||
CrtpService() { services::provide<Type>(static_cast<Type*>(this)); } | ||
~CrtpService() { services::unprovide<Type>(); } | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
#include <bave/core/pinned.hpp> | ||
#include <src/services/services.hpp> | ||
#include <src/tests/test.hpp> | ||
|
||
namespace { | ||
namespace one { | ||
struct Foo : bave::Pinned { | ||
int value{}; | ||
}; | ||
} // namespace one | ||
|
||
namespace two { | ||
struct Foo : bave::Pinned { | ||
int value{}; | ||
}; | ||
} // namespace two | ||
|
||
ADD_TEST(ServicesTypeIntegrity) { | ||
auto foo_one = one::Foo{}; | ||
foo_one.value = 1; | ||
services::provide<one::Foo>(&foo_one); | ||
|
||
auto foo_two = two::Foo{}; | ||
foo_two.value = 2; | ||
services::provide<two::Foo>(&foo_two); | ||
|
||
ASSERT(services::contains<one::Foo>()); | ||
EXPECT(services::get<one::Foo>().value == 1); | ||
ASSERT(services::contains<two::Foo>()); | ||
EXPECT(services::get<two::Foo>().value == 2); | ||
|
||
services::unprovide_all(); | ||
} | ||
|
||
ADD_TEST(ServicesGetException) { | ||
auto thrown = false; | ||
try { | ||
[[maybe_unused]] auto& foo = services::get<one::Foo>(); | ||
} catch (FatalError const&) { thrown = true; } | ||
|
||
EXPECT(thrown); | ||
} | ||
|
||
ADD_TEST(ServicesDuplicateException) { | ||
auto foo = one::Foo{}; | ||
services::provide<one::Foo>(&foo); | ||
|
||
auto thrown = false; | ||
try { | ||
services::provide<one::Foo>(&foo); | ||
} catch (FatalError const&) { thrown = true; } | ||
|
||
EXPECT(thrown); | ||
services::unprovide_all(); | ||
} | ||
|
||
ADD_TEST(ServicesCrtpService) { | ||
struct Bar : CrtpService<Bar> { | ||
int value{}; | ||
}; | ||
|
||
{ | ||
auto bar = Bar{}; | ||
bar.value = 42; | ||
|
||
ASSERT(services::contains<Bar>()); | ||
EXPECT(services::get<Bar>().value == 42); | ||
} | ||
|
||
EXPECT(!services::contains<Bar>()); | ||
services::unprovide_all(); | ||
} | ||
} // namespace |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.