Progress.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Change log
|
||||
|
||||
## v0.0.1 - 2026-04-20 SharedPtrWrapper
|
||||
|
||||
- Initial commit of shared_ptr wrapper for SDL3 structs and functions, with some basic tests. For use as a basis to build RAII wrappers for SDL3 structs and related functions.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Change log
|
||||
|
||||
##### v0.0.1 - 2026-04-20 SharedPtrWrapper
|
||||
+48
-1
@@ -5,7 +5,11 @@ project(hdk-grid
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
#TODO: Project options such as tests, examples, coverage, etc.
|
||||
# Note: The src/ directory contains Doxygen documentation-only .cpp files.
|
||||
# This is a header-only library; src/ files are not compiled into any targets.
|
||||
|
||||
option(HDK_GRID_TESTS "Build hdk-grid tests" ${HDK_TESTS})
|
||||
# No examples for grid at the moment
|
||||
|
||||
add_library(hdk-grid INTERFACE)
|
||||
target_compile_features(hdk-grid INTERFACE cxx_std_17)
|
||||
@@ -17,5 +21,48 @@ target_include_directories(
|
||||
$<INSTALL_INTERFACE:include>
|
||||
)
|
||||
|
||||
if(HDK_GRID_TESTS)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
include(GNUInstallDirs)
|
||||
include(CMakePackageConfigHelpers)
|
||||
|
||||
install(TARGETS hdk-grid
|
||||
EXPORT hdk-gridTargets
|
||||
)
|
||||
|
||||
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
|
||||
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||
)
|
||||
|
||||
install(EXPORT hdk-gridTargets
|
||||
FILE hdk-gridTargets.cmake
|
||||
NAMESPACE hdk::
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hdk-grid
|
||||
)
|
||||
|
||||
configure_package_config_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/cmake/hdk-gridConfig.cmake.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/hdk-gridConfig.cmake
|
||||
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hdk-grid
|
||||
)
|
||||
|
||||
write_basic_package_version_file(
|
||||
${CMAKE_CURRENT_BINARY_DIR}/hdk-gridConfigVersion.cmake
|
||||
VERSION ${PROJECT_VERSION}
|
||||
COMPATIBILITY SameMajorVersion
|
||||
)
|
||||
|
||||
install(FILES
|
||||
${CMAKE_CURRENT_BINARY_DIR}/hdk-gridConfig.cmake
|
||||
${CMAKE_CURRENT_BINARY_DIR}/hdk-gridConfigVersion.cmake
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hdk-grid
|
||||
)
|
||||
|
||||
# TODO: Link to dependencies if we need but keep them minimal and optional.
|
||||
|
||||
# DIRS important for coverage filtering
|
||||
list(APPEND HDK_COVERAGE_DIRS
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
@@ -1,21 +1,64 @@
|
||||
# hdk grid api - holodeck foundation library
|
||||
# hdk grid api - hollow foundation library
|
||||
|
||||
This library provides the foundation for hdk, including core data structures, utilities, and interfaces that other components of the grid will build upon. It serves as the backbone of the system, enabling efficient management of holographic data and interactions.
|
||||
|
||||
(TODO: Reword that last sentence)
|
||||
|
||||
## Shared Struct Wrapper
|
||||
|
||||
The `SharedPtrWrapper` class is a reference-counted wrapper for shared pointers to structures. It provides a mechanism to manage the lifetime of shared resources while allowing for efficient access and caching of shared pointers. The wrapper ensures that shared pointers are properly managed and that resources are released when no longer needed, preventing memory leaks and ensuring efficient memory usage.
|
||||
|
||||
The `SharedPtrWrapper` instances should be able to be used interchangeably with the underlying raw pointers in other C API calls, while still benefiting from the reference counting and caching mechanisms provided by the wrapper.
|
||||
|
||||
## Primitive Wrapper
|
||||
|
||||
## TODO Abstract Event Dispatching
|
||||
The `PrimitiveWrapper` class is a template wrapper for primitive types that provides a consistent interface for getting and setting properties. It allows for easy access to the underlying primitive value while also providing a layer of abstraction that can be useful for managing state and interactions within the grid system. The wrapper can be used to encapsulate primitive values and provide additional functionality, such as validation or transformation, while still allowing for direct access to the underlying value when needed.
|
||||
|
||||
Things to consider:
|
||||
## Event Publishing System (EPS)
|
||||
|
||||
- Define a point to register callbacks.
|
||||
- Support defining return type and arbitrary number of arguments for the callback.
|
||||
- Support, optionally, multiple callbacks for the same event.
|
||||
- Support disconnecting callbacks.
|
||||
`hdk::grid::eps::Conduit<Args...>` is a thread-safe, type-safe event dispatcher. Attach callbacks with `Attach(...)`; each subscription returns a move-only `Tap` handle. Fire the event with `Trigger(...)` or `operator()`. Callbacks run in attachment order from a snapshot taken under lock, so dispatch happens outside the lock and is safe against callbacks that detach or pause other taps.
|
||||
|
||||
`Tap` supports `IsActive()`, `Detach()`, `Pause()`, `Resume()`, and `IsPaused()`. A tap becomes inactive after it is detached or if the underlying conduit is gone.
|
||||
|
||||
`hdk::grid::eps::TapScope` groups multiple taps for bulk management. Add taps with `Add(...)` or `operator<<`, pause/resume them together with `PauseAll()` and `ResumeAll()`, and disconnect everything with `DetachAll()`. Its destructor also calls `DetachAll()`, so any taps still owned by the scope are disconnected automatically. `Clear()` only forgets the stored taps; the subscriptions stay active until detached elsewhere or the scope is destroyed.
|
||||
|
||||
Example usage of `hdk::grid::eps::Conduit` and `Tap`:
|
||||
|
||||
```cpp
|
||||
hdk::grid::eps::Conduit<int> changed;
|
||||
auto tap = changed.Attach([](int value) { /* handle value */ });
|
||||
changed.Trigger(42);
|
||||
tap.Detach();
|
||||
```
|
||||
|
||||
Example usage of `hdk::grid::eps::TapScope` to manage multiple groups of taps:
|
||||
|
||||
1. Somewhere else in the code base there are some conduits:
|
||||
2. Your class that needs to subscribe to some of these:
|
||||
3. Notice that we'vee got two `TapScope` instances, you can have as many as you want.
|
||||
4. Since the `TapScope` instances are members of `MyClass`, they will automatically detach all their taps when an instance of `MyClass` is destroyed, ensuring that there are no dangling subscriptions and that resources are properly cleaned up.
|
||||
|
||||
|
||||
```cpp
|
||||
// #1
|
||||
struct EventData { /* ... */ };
|
||||
hdk::grid::eps::Conduit<EventData*> event;
|
||||
hdk::grid::eps::Conduit<int> changed;
|
||||
hdk::grid::eps::Conduit<void> onMinimized, onRestored, onRender;
|
||||
|
||||
// #2
|
||||
class MyClass {
|
||||
protected:
|
||||
// #3
|
||||
hdk::grid::eps::TapScope genTaps, stateTaps;
|
||||
public:
|
||||
MyClass() {
|
||||
// So let's say you normally do some things a lot of the time...
|
||||
stateTaps << changed.Attach([this](int value) { /* handle value */ });
|
||||
stateTaps << event.Attach([](EventData* data) { /* handle event */ });
|
||||
stateTaps << onRender.Attach([]() { /* do render stuff */ });
|
||||
// But sometimes you want to pause that stuff, but not everything else.
|
||||
genTaps << onMinimized.Attach([this]() { stateTaps.PauseAll(); });
|
||||
genTaps << onRestored.Attach([this]() { stateTaps.ResumeAll(); });
|
||||
// Notice the genTaps group is always active and those handlers are used to control the other stateTaps group.
|
||||
}
|
||||
}; // # 4
|
||||
```
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/hdk-gridTargets.cmake")
|
||||
check_required_components(hdk-grid)
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
/// @file AABB.hpp
|
||||
/// For complete documentation, see src/AABB.cpp
|
||||
|
||||
namespace hdk::grid {
|
||||
template <int N>
|
||||
class AABB {
|
||||
/// @todo Implement Axis Aligned Bounding Box virtual interface class or remove this file if not needed
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
#pragma once
|
||||
/// @file AbstractEventDispatch.hpp
|
||||
/// For complete documentation, see src/AbstractEventDispatch.cpp
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
namespace hdk::grid::eps {
|
||||
template <typename... ArgTypes> class Conduit {
|
||||
private:
|
||||
struct State;
|
||||
|
||||
public:
|
||||
typedef std::function<void(ArgTypes...)> Callback_t;
|
||||
|
||||
class Tap {
|
||||
public:
|
||||
Tap() = default;
|
||||
Tap(const Tap&) = delete;
|
||||
Tap& operator=(const Tap&) = delete;
|
||||
Tap(Tap&& other) noexcept
|
||||
: state(std::move(other.state)), id(other.id) {
|
||||
other.id = 0;
|
||||
}
|
||||
Tap& operator=(Tap&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Detach();
|
||||
state = std::move(other.state);
|
||||
id = other.id;
|
||||
other.id = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool IsActive() const {
|
||||
auto locked = state.lock();
|
||||
if (!locked || id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(locked->mutex);
|
||||
return locked->index.find(id) != locked->index.end();
|
||||
}
|
||||
|
||||
bool Detach() {
|
||||
auto locked = state.lock();
|
||||
if (!locked || id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(locked->mutex);
|
||||
auto it = locked->index.find(id);
|
||||
if (it == locked->index.end()) {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
locked->entries.erase(it->second);
|
||||
locked->index.erase(it);
|
||||
id = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Pause() {
|
||||
auto locked = state.lock();
|
||||
if (!locked || id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(locked->mutex);
|
||||
auto it = locked->index.find(id);
|
||||
if (it == locked->index.end()) {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
it->second->paused = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Resume() {
|
||||
auto locked = state.lock();
|
||||
if (!locked || id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(locked->mutex);
|
||||
auto it = locked->index.find(id);
|
||||
if (it == locked->index.end()) {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
it->second->paused = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsPaused() const {
|
||||
auto locked = state.lock();
|
||||
if (!locked || id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(locked->mutex);
|
||||
auto it = locked->index.find(id);
|
||||
if (it == locked->index.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second->paused;
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Conduit;
|
||||
Tap(std::weak_ptr<State> subscription_state, std::uint64_t subscription_id)
|
||||
: state(std::move(subscription_state)), id(subscription_id) { }
|
||||
|
||||
std::weak_ptr<State> state;
|
||||
std::uint64_t id = 0;
|
||||
};
|
||||
|
||||
virtual ~Conduit() = default;
|
||||
|
||||
virtual Tap Attach(Callback_t callback) {
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
const std::uint64_t id = state->next_id++;
|
||||
auto it = state->entries.insert(state->entries.end(), Entry{ id, std::move(callback), false });
|
||||
state->index.emplace(id, it);
|
||||
return Tap(state, id);
|
||||
}
|
||||
|
||||
virtual bool Detach(std::uint64_t id) {
|
||||
if (id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
auto it = state->index.find(id);
|
||||
if (it == state->index.end()) {
|
||||
return false;
|
||||
}
|
||||
state->entries.erase(it->second);
|
||||
state->index.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool Pause(std::uint64_t id) {
|
||||
if (id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
auto it = state->index.find(id);
|
||||
if (it == state->index.end()) {
|
||||
return false;
|
||||
}
|
||||
it->second->paused = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool Resume(std::uint64_t id) {
|
||||
if (id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
auto it = state->index.find(id);
|
||||
if (it == state->index.end()) {
|
||||
return false;
|
||||
}
|
||||
it->second->paused = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual bool IsPaused(std::uint64_t id) const {
|
||||
if (id == 0) {
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
auto it = state->index.find(id);
|
||||
if (it == state->index.end()) {
|
||||
return false;
|
||||
}
|
||||
return it->second->paused;
|
||||
}
|
||||
|
||||
virtual void Trigger(ArgTypes... args) const {
|
||||
std::vector<Callback_t> callbacks;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(state->mutex);
|
||||
callbacks.reserve(state->entries.size());
|
||||
for (const auto& entry : state->entries) {
|
||||
if (!entry.paused) {
|
||||
callbacks.push_back(entry.callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& callback : callbacks) {
|
||||
callback(args...);
|
||||
}
|
||||
}
|
||||
|
||||
void operator()(ArgTypes... args) const { Trigger(args...); }
|
||||
Tap operator%(Callback_t callback) { return Attach(std::move(callback)); }
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
std::uint64_t id;
|
||||
Callback_t callback;
|
||||
bool paused;
|
||||
};
|
||||
|
||||
struct State {
|
||||
std::mutex mutex;
|
||||
std::list<Entry> entries;
|
||||
std::unordered_map<std::uint64_t, typename std::list<Entry>::iterator> index;
|
||||
std::uint64_t next_id = 1;
|
||||
};
|
||||
|
||||
std::shared_ptr<State> state = std::make_shared<State>();
|
||||
};
|
||||
|
||||
class TapScope {
|
||||
public:
|
||||
TapScope() = default;
|
||||
TapScope(const TapScope&) = delete;
|
||||
TapScope& operator=(const TapScope&) = delete;
|
||||
TapScope(TapScope&&) = delete;
|
||||
TapScope& operator=(TapScope&&) = delete;
|
||||
~TapScope() { DetachAll(); }
|
||||
|
||||
template <typename TapT> void Add(TapT tap) {
|
||||
if (!tap.IsActive()) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
entries.emplace_back(std::make_unique<TapEntry<TapT>>(std::move(tap)));
|
||||
}
|
||||
|
||||
template <typename TapT> TapScope& operator<<(TapT tap) {
|
||||
Add(std::move(tap));
|
||||
return *this;
|
||||
}
|
||||
|
||||
void DetachAll() {
|
||||
std::vector<std::unique_ptr<TapEntryBase>> to_run;
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
to_run.swap(entries);
|
||||
}
|
||||
for (auto& tap : to_run) {
|
||||
tap->Detach();
|
||||
}
|
||||
}
|
||||
|
||||
void PauseAll() {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
for (auto& tap : entries) {
|
||||
tap->Pause();
|
||||
}
|
||||
}
|
||||
|
||||
void ResumeAll() {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
for (auto& tap : entries) {
|
||||
tap->Resume();
|
||||
}
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
entries.clear();
|
||||
}
|
||||
|
||||
std::size_t Size() const {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
struct TapEntryBase {
|
||||
virtual ~TapEntryBase() = default;
|
||||
virtual void Detach() = 0;
|
||||
virtual void Pause() = 0;
|
||||
virtual void Resume() = 0;
|
||||
};
|
||||
|
||||
template <typename TapT> struct TapEntry : TapEntryBase {
|
||||
explicit TapEntry(TapT&& tap_value)
|
||||
: tap(std::move(tap_value)) { }
|
||||
|
||||
void Detach() override { tap.Detach(); }
|
||||
void Pause() override { tap.Pause(); }
|
||||
void Resume() override { tap.Resume(); }
|
||||
|
||||
TapT tap;
|
||||
};
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::vector<std::unique_ptr<TapEntryBase>> entries;
|
||||
|
||||
};
|
||||
} // namespace hdk::grid::eps
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
/// @file PrimitiveWrapper.hpp
|
||||
/// For complete documentation, see src/PrimitiveWrapper.cpp
|
||||
//
|
||||
namespace hdk::grid {
|
||||
template <typename T>
|
||||
class PrimitiveWrapper {
|
||||
private:
|
||||
T primitive_value;
|
||||
public:
|
||||
PrimitiveWrapper() = delete;
|
||||
PrimitiveWrapper(T value) : primitive_value(value) {}
|
||||
operator T() const { return primitive_value; }
|
||||
PrimitiveWrapper& operator=(T newValue) {
|
||||
primitive_value = newValue;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,48 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
/// @file SharedPtrWrapper.hpp
|
||||
/// For complete documentation, see src/SharedPtrWrapper.cpp
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace hdk::grid {
|
||||
/**
|
||||
* @brief Reference-counted wrapper for shared pointers to structures.
|
||||
* @remarks
|
||||
* Has no default constructor. Must be constructed with a value or explicitly set initialize to nullptr.
|
||||
* Evaluates to false if shared pointer is null, true otherwise.
|
||||
* Will automatically cast to the `StructType*`, allowing it to be used in C API calls that expect raw pointers, while still benefiting from the reference counting and caching mechanisms provided by the wrapper.
|
||||
*/
|
||||
template <typename StructType>
|
||||
class SharedPtrWrapper {
|
||||
public:
|
||||
/** Type alias for a shared pointer to the managed structure. **/
|
||||
template <typename StructType> class SharedPtrWrapper {
|
||||
public:
|
||||
using SharedPtr = std::shared_ptr<StructType>;
|
||||
/** Type alias for a weak pointer to the managed structure. **/
|
||||
using WeakPtr = std::weak_ptr<StructType>;
|
||||
/** Type alias for the cache map that maps raw pointers to weak pointers. **/
|
||||
using SharedCacheMap = std::unordered_map<StructType*, WeakPtr>;
|
||||
/** Function type for deleting the managed structure. **/
|
||||
using DeleteFunc = std::function<void(StructType*)>;
|
||||
static void null_deleter(StructType*) { }
|
||||
/** Default constructor deleted. You must declare a value or declare it to be null on construction. **/
|
||||
SharedPtrWrapper() = delete;
|
||||
/** Null constructor (same as default constructor). **/
|
||||
SharedPtrWrapper(std::nullptr_t)
|
||||
: shared_struct(nullptr)
|
||||
{
|
||||
}
|
||||
/** Constructor that takes a shared pointer and a delete function. **/
|
||||
: shared_struct(nullptr) { }
|
||||
SharedPtrWrapper(StructType* struct_ptr, DeleteFunc deleteFunc)
|
||||
: shared_struct(struct_ptr, deleteFunc)
|
||||
{
|
||||
: shared_struct(struct_ptr, deleteFunc) {
|
||||
// Cache the shared pointer to ensure consistent reference counting for the same raw pointer.
|
||||
get_shared_weak_ptr_cache()[struct_ptr] = shared_struct;
|
||||
}
|
||||
/** Move constructor. **/
|
||||
SharedPtrWrapper(SharedPtrWrapper&& other) noexcept
|
||||
: shared_struct(std::move(other.shared_struct))
|
||||
{
|
||||
}
|
||||
: shared_struct(std::move(other.shared_struct)) { }
|
||||
/** Existing Shared Pointer */
|
||||
SharedPtrWrapper(SharedPtr shared_ptr)
|
||||
|
||||
@@ -52,71 +31,40 @@ public:
|
||||
get_shared_weak_ptr_cache()[shared_ptr.get()] = shared_struct;
|
||||
}
|
||||
}
|
||||
/** Move assignment operator. **/
|
||||
SharedPtrWrapper& operator=(SharedPtrWrapper&& other) noexcept
|
||||
{
|
||||
SharedPtrWrapper& operator=(SharedPtrWrapper&& other) noexcept {
|
||||
if (this != &other) {
|
||||
shared_struct = std::move(other.shared_struct);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
/** Copy constructor. **/
|
||||
SharedPtrWrapper(const SharedPtrWrapper& other)
|
||||
: shared_struct(other.shared_struct)
|
||||
{
|
||||
}
|
||||
/** Copy assignment operator. **/
|
||||
SharedPtrWrapper& operator=(const SharedPtrWrapper& other)
|
||||
{
|
||||
: shared_struct(other.shared_struct) { }
|
||||
SharedPtrWrapper& operator=(const SharedPtrWrapper& other) {
|
||||
if (this != &other) {
|
||||
shared_struct = other.shared_struct;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
/** Conversion operator to allow automatic casting to StructType*. **/
|
||||
operator StructType*() const
|
||||
{
|
||||
return shared_struct.get();
|
||||
}
|
||||
/** Set this instance to nullptr. **/
|
||||
SharedPtrWrapper& operator=(std::nullptr_t)
|
||||
{
|
||||
operator StructType*() const { return shared_struct.get(); }
|
||||
SharedPtrWrapper& operator=(std::nullptr_t) {
|
||||
shared_struct.reset();
|
||||
return *this;
|
||||
}
|
||||
/** Conversion operator to allow automatic casting to bool. **/
|
||||
operator bool() const
|
||||
{
|
||||
return shared_struct != nullptr;
|
||||
}
|
||||
operator bool() const { return shared_struct != nullptr; }
|
||||
StructType* operator->() const { return shared_struct.get(); }
|
||||
private: // internal use only.
|
||||
SharedPtr shared_struct { nullptr };
|
||||
|
||||
private: // internal use only.
|
||||
/** The value we are wrapping. **/
|
||||
SharedPtr shared_struct{nullptr};
|
||||
|
||||
protected: // For use by derived classes.
|
||||
/** So that we provide consistent reference counting on the same pointer we need to have a cache of shared pointers. This is because if we create multiple shared pointers to the same raw pointer, they will have separate reference counts and may lead to double deletion. **/
|
||||
static SharedCacheMap& get_shared_weak_ptr_cache()
|
||||
{
|
||||
protected: // For use by derived classes.
|
||||
static SharedCacheMap& get_shared_weak_ptr_cache() {
|
||||
static SharedCacheMap cache_map;
|
||||
return cache_map;
|
||||
}
|
||||
/** Helper function to check if a raw pointer is already cached and has a valid shared pointer. **/
|
||||
static bool is_cached(StructType* ptr)
|
||||
{
|
||||
static bool is_cached(StructType* ptr) {
|
||||
auto& cache = get_shared_weak_ptr_cache();
|
||||
return cache.find(ptr) != cache.end() && !cache[ptr].expired();
|
||||
}
|
||||
/** @brief helper: if cached returns existing, else returns non-owning shared pointer without caching it.
|
||||
* @remarks
|
||||
* The idea is that if we are already managing this pointer
|
||||
* we should return the existing shared pointer to ensure consistent reference counting
|
||||
* but if we are not managing this pointer, we can create a new shared pointer with a null deleter
|
||||
* so that we can still return a shared pointer without taking ownership of the pointer.
|
||||
* @return Always a valid shared pointer to `ptr`, but it may be a non-owning shared pointer if `ptr` is not already cached.
|
||||
***/
|
||||
static SharedPtr get_or_view(StructType* ptr)
|
||||
{
|
||||
static SharedPtr get_or_view(StructType* ptr) {
|
||||
/** if no pointer */
|
||||
if (!ptr) {
|
||||
/** then return a non-owning null */
|
||||
@@ -131,18 +79,7 @@ protected: // For use by derived classes.
|
||||
return new_shared;
|
||||
}
|
||||
}
|
||||
/** @brief helper: if cached returns existing, else constructs shared pointer with deleter and caches.
|
||||
* @remarks
|
||||
* The idea is that if we are already managing this pointer
|
||||
* we should return the existing shared pointer to ensure consistent reference counting
|
||||
* but if we are not managing this pointer, we can create a new shared pointer taking ownership.
|
||||
* When this pointer is given in the future, as long as it is still valid, will return the same shared pointer.
|
||||
* @param ptr the pointer to either get or create for
|
||||
* @param deleteFunc the delete function used if creating
|
||||
* @return SharedPtr either precached or created and cached
|
||||
*/
|
||||
static SharedPtr get_or_cache(StructType* ptr, DeleteFunc deleteFunc)
|
||||
{
|
||||
static SharedPtr get_or_cache(StructType* ptr, DeleteFunc deleteFunc) {
|
||||
/** If no ptr, no need to do anything else */
|
||||
if (!ptr) {
|
||||
return SharedPtr(nullptr, deleteFunc);
|
||||
@@ -156,5 +93,5 @@ protected: // For use by derived classes.
|
||||
return new_shared;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* @file AbstractEventDispatch.hpp
|
||||
* @brief Documentation for event dispatch system - publish-subscribe pattern for callbacks
|
||||
*
|
||||
* AbstractEventDispatch provides a thread-safe event system based on the observer/publish-subscribe
|
||||
* pattern. It allows subscribers to register callbacks for events and broadcasters to trigger
|
||||
* events that invoke all registered callbacks. This file contains the complete documentation.
|
||||
* The actual implementation is header-only in AbstractEventDispatch.hpp.
|
||||
*/
|
||||
|
||||
namespace hdk::grid {
|
||||
// ============================================================================
|
||||
// Conduit Template - Core Documentation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @class Conduit
|
||||
* @brief Generic event dispatcher template for publish-subscribe pattern
|
||||
*
|
||||
* Conduit implements a thread-safe event system where subscribers can register
|
||||
* callbacks for events and broadcasters can trigger events. All operations are protected by
|
||||
* a shared mutex to ensure thread safety.
|
||||
*
|
||||
* @tparam ArgTypes The argument types passed to callbacks when the event is triggered
|
||||
*
|
||||
* Key features:
|
||||
* - Thread-safe callback management and invocation
|
||||
* - RAII subscription lifetime management via Tap objects
|
||||
* - Lightweight ID-based subscription tracking
|
||||
* - Efficient callback invocation without holding locks
|
||||
*
|
||||
* @note All callbacks must have void return type
|
||||
* @note Callbacks should not throw exceptions (undefined behavior if they do)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef Conduit::Callback_t
|
||||
* @brief Type alias for event callback function
|
||||
*
|
||||
* A callback is a function that takes the specified ArgTypes and returns void.
|
||||
* Example: std::function<void(int, const std::string&)>
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Conduit::Tap - Lifetime Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @class Conduit::Tap
|
||||
* @brief RAII handle for managing event subscription lifetime
|
||||
*
|
||||
* Tap objects represent an active subscription to an event. They use move semantics
|
||||
* and automatically unsubscribe when moved-from or destroyed. This ensures callbacks are
|
||||
* properly cleaned up without manual management.
|
||||
*
|
||||
* Features:
|
||||
* - Move-only semantics (non-copyable)
|
||||
* - Automatic unsubscription on destruction
|
||||
* - Query subscription status with IsActive()
|
||||
* - Manual unsubscription with Detach()
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* Conduit<int> event;
|
||||
* auto sub = event.Attach([](int value) { std::cout << value << std::endl; });
|
||||
* event.Trigger(42); // Prints: 42
|
||||
* sub.Detach();
|
||||
* event.Trigger(100); // No output - subscription removed
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn bool Conduit::Tap::IsActive() const
|
||||
* @brief Check if this subscription is still active
|
||||
*
|
||||
* @return true if the subscription is active and will receive events, false otherwise
|
||||
* @note A subscription becomes inactive after Detach() is called or the dispatcher is destroyed
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn bool Conduit::Tap::Detach()
|
||||
* @brief Manually unsubscribe from the event
|
||||
*
|
||||
* Removes this subscription from the dispatcher so the callback will no longer be invoked.
|
||||
* Safe to call multiple times - subsequent calls return false.
|
||||
*
|
||||
* @return true if unsubscription was successful, false if already unsubscribed
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Conduit - Event Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn Tap Conduit::Attach(Callback_t callback)
|
||||
* @brief Register a callback for this event
|
||||
*
|
||||
* Adds a callback to the list of subscriptions. The callback will be invoked whenever
|
||||
* Trigger() is called, with the provided arguments passed to the callback.
|
||||
*
|
||||
* @param callback The callback function to register (std::function)
|
||||
* @return Tap An RAII handle that manages the subscription lifetime
|
||||
*
|
||||
* @note Callback must be callable with the template ArgTypes
|
||||
* @note Move semantics: callback is moved into the dispatcher
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn bool Conduit::Detach(std::uint64_t id)
|
||||
* @brief Detach by subscription ID
|
||||
*
|
||||
* Removes a subscription by its numeric ID. This is used internally by Tap objects
|
||||
* but can also be called directly if needed.
|
||||
*
|
||||
* @param id The subscription ID to remove
|
||||
* @return true if unsubscription was successful, false if ID not found
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn void Conduit::Trigger(ArgTypes... args)
|
||||
* @brief Trigger the event, invoking all registered callbacks
|
||||
*
|
||||
* Invokes all active callbacks with the provided arguments. Callbacks are invoked sequentially
|
||||
* in the order they were registered. If a callback throws an exception, behavior is undefined.
|
||||
*
|
||||
* Thread safety: This method acquires a lock to copy callbacks, then releases it before
|
||||
* invocation. This allows callbacks to subscribe/unsubscribe during execution, but the
|
||||
* newly subscribed callbacks will be invoked on the next Trigger call.
|
||||
*
|
||||
* @param args Arguments to pass to each callback
|
||||
*
|
||||
* @note This method is thread-safe
|
||||
* @note Safe to call from multiple threads concurrently
|
||||
* @note Safe for callbacks to subscribe/unsubscribe (though changes take effect next trigger)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn void Conduit::operator()(ArgTypes... args)
|
||||
* @brief Convenience operator for triggering the event
|
||||
*
|
||||
* Equivalent to Trigger(args...). Allows the dispatcher to be called like a function.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// TapScope - Batch Tap Management
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @class TapScope
|
||||
* @brief RAII container for managing multiple subscriptions as a group
|
||||
*
|
||||
* TapScope simplifies managing multiple related subscriptions by collecting them
|
||||
* in one object. When the TapScope is destroyed or DetachAll() is called,
|
||||
* all contained subscriptions are automatically unsubscribed.
|
||||
*
|
||||
* This is useful for managing event subscriptions tied to an object's lifetime - simply
|
||||
* add all subscriptions to a TapScope member and they'll be cleaned up automatically
|
||||
* when the object is destroyed.
|
||||
*
|
||||
* Features:
|
||||
* - Add multiple subscriptions from different dispatchers
|
||||
* - Automatic cleanup of all subscriptions on destruction
|
||||
* - Thread-safe subscription management
|
||||
* - Move-only semantics
|
||||
*
|
||||
* Example usage:
|
||||
* @code
|
||||
* class MyWidget {
|
||||
* private:
|
||||
* Conduit<int> on_click;
|
||||
* Conduit<std::string> on_text_changed;
|
||||
* hdk::grid::TapScope subscriptions;
|
||||
*
|
||||
* public:
|
||||
* MyWidget() {
|
||||
* subscriptions.Add(on_click.Attach([](int button) { ... }));
|
||||
* subscriptions.Add(on_text_changed.Attach([](const std::string& text) { ... }));
|
||||
* }
|
||||
* // Subscriptions automatically unsubscribed when MyWidget is destroyed
|
||||
* };
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn template <typename SubscriptionT> void TapScope::Add(SubscriptionT subscription)
|
||||
* @brief Add a subscription to this group
|
||||
*
|
||||
* Takes ownership of the subscription. When DetachAll() is called or the group is
|
||||
* destroyed, this subscription will be unsubscribed.
|
||||
*
|
||||
* @tparam SubscriptionT The subscription type (typically Conduit::Tap)
|
||||
* @param subscription The subscription to add (moved into the group)
|
||||
*
|
||||
* @note Has no effect if the subscription is not active
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn template <typename SubscriptionT> TapScope& TapScope::operator<<(SubscriptionT subscription)
|
||||
* @brief Fluent API for adding subscriptions
|
||||
*
|
||||
* Provides a more concise syntax for adding multiple subscriptions:
|
||||
* @code
|
||||
* group << dispatcher1.Attach(callback1)
|
||||
* << dispatcher2.Attach(callback2)
|
||||
* << dispatcher3.Attach(callback3);
|
||||
* @endcode
|
||||
*
|
||||
* @tparam SubscriptionT The subscription type
|
||||
* @param subscription The subscription to add (moved into the group)
|
||||
* @return Reference to this group for chaining
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn void TapScope::DetachAll()
|
||||
* @brief Detach all contained subscriptions
|
||||
*
|
||||
* Immediately removes all subscriptions in this group. Called automatically on destruction.
|
||||
* Safe to call multiple times.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn void TapScope::Clear()
|
||||
* @brief Clear all subscriptions without unsubscribing
|
||||
*
|
||||
* Removes all subscriptions from this group without calling Detach(). The subscriptions
|
||||
* remain active (tied to their original dispatchers).
|
||||
*
|
||||
* @note Rarely needed - use DetachAll() in most cases
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn std::size_t TapScope::Size() const
|
||||
* @brief Get the number of subscriptions in this group
|
||||
*
|
||||
* @return The number of subscriptions currently in this group
|
||||
*/
|
||||
|
||||
} // namespace hdk::grid
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @file PrimitiveWrapper.hpp
|
||||
* @brief Documentation for PrimitiveWrapper template - lightweight wrapper for primitive types
|
||||
*
|
||||
* PrimitiveWrapper provides a simple wrapper for primitive C types to provide consistent interface
|
||||
* and type safety. This file contains the complete documentation. The actual implementation
|
||||
* is header-only in PrimitiveWrapper.hpp.
|
||||
*/
|
||||
|
||||
namespace hdk::grid {
|
||||
// ============================================================================
|
||||
// PrimitiveWrapper Template - Core Documentation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @class PrimitiveWrapper
|
||||
* @brief Lightweight wrapper for primitive C types
|
||||
*
|
||||
* @tparam T The primitive type being wrapped (e.g., SDL_PixelFormat, SDL_DisplayID, SDL_PropertiesID)
|
||||
*
|
||||
* PrimitiveWrapper provides a simple, zero-overhead wrapper for primitive types. Unlike SharedPtrWrapper,
|
||||
* PrimitiveWrapper is designed for types that don't need lifetime management—they're either simple IDs
|
||||
* or owned by SDL or other external systems.
|
||||
*
|
||||
* PrimitiveWrapper provides:
|
||||
* - Type safety: Prevents accidental mixing of different ID types
|
||||
* - Consistency: Uniform interface with SharedPtrWrapper
|
||||
* - Zero overhead: Compiles down to the primitive type itself
|
||||
* - Implicit conversion: Can be used anywhere the primitive type is expected
|
||||
*
|
||||
* Common uses:
|
||||
* - Wrapping ID types (SDL_WindowID, SDL_DisplayID, SDL_TextureID)
|
||||
* - Wrapping enumerated types (SDL_PixelFormat, SDL_BlendMode)
|
||||
* - Wrapping other primitive types (SDL_PropertiesID)
|
||||
*
|
||||
* @see SharedPtrWrapper
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Constructor and Initialization
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn PrimitiveWrapper::PrimitiveWrapper(T value)
|
||||
* @brief Construct with a primitive value
|
||||
*
|
||||
* @param value The primitive value to wrap
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* hdk::sdl::PixelFormat format(SDL_PIXELFORMAT_RGBA8888);
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Conversion Operators
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn PrimitiveWrapper::operator T() const
|
||||
* @brief Implicit conversion to the wrapped primitive type
|
||||
*
|
||||
* Allows the wrapper to be used anywhere the primitive type is expected, providing
|
||||
* seamless compatibility with C APIs.
|
||||
*
|
||||
* @return T The wrapped primitive value
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* hdk::sdl::PixelFormat format = ...;
|
||||
* const char* name = SDL_GetPixelFormatName(format); // implicit conversion to SDL_PixelFormat
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Assignment
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn PrimitiveWrapper& PrimitiveWrapper::operator=(T newValue)
|
||||
* @brief Assign a new primitive value
|
||||
*
|
||||
* @param newValue The new value to assign
|
||||
* @return Reference to this wrapper
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* @file SharedPtrWrapper.hpp
|
||||
* @brief Documentation for SharedPtrWrapper template - reference-counted wrapper for C structures
|
||||
*
|
||||
* SharedPtrWrapper provides a template base class for wrapping C-style structures with shared_ptr
|
||||
* lifetime management. This file contains the complete documentation. The actual implementation
|
||||
* is header-only in SharedPtrWrapper.hpp.
|
||||
*/
|
||||
|
||||
namespace hdk::grid {
|
||||
// ============================================================================
|
||||
// SharedPtrWrapper Template - Core Documentation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @class SharedPtrWrapper
|
||||
* @brief Reference-counted wrapper for C structures using std::shared_ptr
|
||||
*
|
||||
* @tparam StructType The C structure type being wrapped
|
||||
*
|
||||
* SharedPtrWrapper provides automatic lifetime management for C-style structures through reference counting.
|
||||
* This template is the foundation for building C++-style wrappers around C APIs while maintaining compatibility
|
||||
* with the underlying C code through implicit pointer conversion.
|
||||
*
|
||||
* Key features:
|
||||
* - Reference counting: Multiple copies of a wrapper share ownership of one C structure
|
||||
* - Implicit casting: Automatically converts to StructType* for C API compatibility
|
||||
* - Smart pointer semantics: Automatic cleanup when last reference is released
|
||||
* - Caching: Prevents reference counting issues when the same C pointer is wrapped multiple times
|
||||
*
|
||||
* The template maintains an internal cache of weak pointers to ensure that the same C structure
|
||||
* pointer always maps to the same shared_ptr instance, preventing double-deletion and ensuring
|
||||
* consistent reference counting.
|
||||
*
|
||||
* @note No default constructor. Must be constructed with a value or explicitly set to nullptr.
|
||||
* @note Evaluates to false (via operator bool) if the shared pointer is null, true otherwise.
|
||||
*
|
||||
* @see https://en.cppreference.com/w/cpp/memory/shared_ptr
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Constructor and Initialization
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::SharedPtrWrapper(std::nullptr_t)
|
||||
* @brief Construct a null wrapper (explicit nullptr initialization)
|
||||
*
|
||||
* Initializes the wrapper to hold a null pointer.
|
||||
*
|
||||
* @see SharedPtrWrapper::operator=(std::nullptr_t)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::SharedPtrWrapper(StructType* struct_ptr, DeleteFunc deleteFunc)
|
||||
* @brief Construct from a raw pointer and deletion function
|
||||
*
|
||||
* Creates a wrapper that takes ownership of the provided pointer and uses the specified
|
||||
* deletion function for cleanup when the reference count reaches zero.
|
||||
*
|
||||
* @tparam StructType The structure type
|
||||
* @param struct_ptr The raw pointer to wrap
|
||||
* @param deleteFunc The function to call for cleanup (e.g., SDL_DestroyWindow, SDL_DestroyRenderer)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::SharedPtrWrapper(SharedPtr shared_ptr)
|
||||
* @brief Construct from an existing shared_ptr
|
||||
*
|
||||
* Creates a wrapper around an already-created shared_ptr, which is useful when working with
|
||||
* already-managed pointers or integrating with other code that uses shared_ptr.
|
||||
*
|
||||
* @param shared_ptr An existing std::shared_ptr to wrap
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::SharedPtrWrapper(SharedPtrWrapper&& other) noexcept
|
||||
* @brief Move constructor
|
||||
*
|
||||
* Efficiently transfers ownership from another wrapper instance.
|
||||
*
|
||||
* @param other The source wrapper (will be moved from)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::SharedPtrWrapper(const SharedPtrWrapper& other)
|
||||
* @brief Copy constructor
|
||||
*
|
||||
* Creates a new wrapper that shares ownership of the same structure. Both instances will
|
||||
* increment the reference count and share the lifetime of the underlying structure.
|
||||
*
|
||||
* @param other The source wrapper to copy from
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Assignment and Conversion
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper& SharedPtrWrapper::operator=(SharedPtrWrapper&& other) noexcept
|
||||
* @brief Move assignment operator
|
||||
*
|
||||
* @param other The source wrapper (will be moved from)
|
||||
* @return Reference to this wrapper
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper& SharedPtrWrapper::operator=(const SharedPtrWrapper& other)
|
||||
* @brief Copy assignment operator
|
||||
*
|
||||
* @param other The source wrapper to copy from
|
||||
* @return Reference to this wrapper
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper& SharedPtrWrapper::operator=(std::nullptr_t)
|
||||
* @brief Assign nullptr to release ownership
|
||||
*
|
||||
* Releases ownership of the current structure, decrementing the reference count.
|
||||
* If this was the last reference, the structure is destroyed using the registered deletion function.
|
||||
*
|
||||
* @return Reference to this wrapper
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::operator StructType*() const
|
||||
* @brief Implicit conversion to raw C pointer
|
||||
*
|
||||
* Allows the wrapper to be used anywhere a StructType* is expected, enabling seamless
|
||||
* compatibility with C APIs.
|
||||
*
|
||||
* @return StructType* The underlying raw pointer
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* hdk::sdl::Window window = ...;
|
||||
* SDL_SetWindowSize(window, 800, 600); // implicit conversion to SDL_Window*
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn SharedPtrWrapper::operator bool() const
|
||||
* @brief Check if the wrapper holds a valid pointer
|
||||
*
|
||||
* @return bool True if holding a non-null pointer, false if null
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* if (window) {
|
||||
* // window is valid
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn StructType* SharedPtrWrapper::operator->() const
|
||||
* @brief Arrow operator for member access
|
||||
*
|
||||
* Allows access to structure members through the -> operator.
|
||||
*
|
||||
* @return StructType* The underlying pointer for member access
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* hdk::grid::SharedPtrWrapper<SDL_Surface> surface = ...;
|
||||
* int width = surface->w; // access surface width
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Type Aliases
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @typedef SharedPtrWrapper::SharedPtr
|
||||
* @brief Type alias for std::shared_ptr<StructType>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef SharedPtrWrapper::WeakPtr
|
||||
* @brief Type alias for std::weak_ptr<StructType>
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef SharedPtrWrapper::SharedCacheMap
|
||||
* @brief Type alias for the cache map (std::unordered_map<StructType*, WeakPtr>)
|
||||
*
|
||||
* The cache ensures that multiple wrappers of the same raw pointer share the same
|
||||
* shared_ptr instance, preventing reference counting errors.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef SharedPtrWrapper::DeleteFunc
|
||||
* @brief Type alias for deletion function (std::function<void(StructType*)>)
|
||||
*
|
||||
* The deletion function is called when the reference count reaches zero.
|
||||
* Typically this is a C API cleanup function like SDL_DestroyWindow.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Protected Helper Methods (for derived classes)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @fn static SharedPtr SharedPtrWrapper::get_or_view(StructType* ptr)
|
||||
* @brief Get or create a non-owning reference to a C pointer
|
||||
*
|
||||
* This protected method is used by derived classes to create references to C pointers
|
||||
* without taking ownership. If the pointer is already in the cache, returns the cached
|
||||
* shared_ptr. Otherwise, returns a new non-owning shared_ptr (with null_deleter).
|
||||
*
|
||||
* This is useful when:
|
||||
* - The C object was created outside this wrapper system
|
||||
* - You want to reference a C object without managing its lifetime
|
||||
* - The C object is owned by SDL or other external code
|
||||
*
|
||||
* @param ptr The raw pointer to wrap
|
||||
* @return SharedPtr A shared_ptr to the pointer (owning if cached, non-owning otherwise)
|
||||
*
|
||||
* Example from derived class:
|
||||
* @code
|
||||
* // Getting a window created externally, without taking ownership
|
||||
* static Window GetFromID(SDL_WindowID id) {
|
||||
* return Window(get_or_view(SDL_GetWindowFromID(id)));
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @fn static SharedPtr SharedPtrWrapper::get_or_cache(StructType* ptr, DeleteFunc deleteFunc)
|
||||
* @brief Get or create an owning reference to a C pointer
|
||||
*
|
||||
* This protected method is used by derived classes to create owning references to C pointers.
|
||||
* If the pointer is already in the cache, returns the cached shared_ptr. Otherwise, creates
|
||||
* a new shared_ptr with the provided deletion function and caches it.
|
||||
*
|
||||
* This ensures that the same C pointer always maps to the same shared_ptr instance,
|
||||
* preventing reference counting errors.
|
||||
*
|
||||
* @param ptr The raw pointer to wrap
|
||||
* @param deleteFunc The function to call when the reference count reaches zero
|
||||
* @return SharedPtr A shared_ptr to the pointer (cached if newly created)
|
||||
*
|
||||
* Example from derived class:
|
||||
* @code
|
||||
* // Creating a new window and taking ownership
|
||||
* static Window Create(const char* title, int w, int h, SDL_WindowFlags flags) {
|
||||
* return Window(get_or_cache(SDL_CreateWindow(title, w, h, flags), SDL_DestroyWindow));
|
||||
* }
|
||||
* @endcode
|
||||
*/
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
add_executable(
|
||||
hdk-grid-tests
|
||||
main.cpp
|
||||
PrimitiveWrapper_test.cpp
|
||||
SharedPtrWrapper_test.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(hdk-grid-tests PRIVATE hdk-grid doctest::doctest)
|
||||
target_compile_features(hdk-grid-tests PRIVATE cxx_std_17)
|
||||
|
||||
if(HDK_COVERAGE AND TARGET hdk-coverage-flags)
|
||||
target_link_libraries(hdk-grid-tests PRIVATE hdk-coverage-flags)
|
||||
endif()
|
||||
|
||||
add_test(NAME hdk-grid-tests COMMAND hdk-grid-tests)
|
||||
@@ -0,0 +1,18 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <hdk/grid/PrimitiveWrapper.hpp>
|
||||
|
||||
TEST_CASE("PrimitiveWrapper stores and exposes primitive values") {
|
||||
hdk::grid::PrimitiveWrapper<int> wrapped(7);
|
||||
CHECK(static_cast<int>(wrapped) == 7);
|
||||
|
||||
wrapped = 42;
|
||||
CHECK(static_cast<int>(wrapped) == 42);
|
||||
}
|
||||
|
||||
TEST_CASE("PrimitiveWrapper assignment returns self for chaining") {
|
||||
hdk::grid::PrimitiveWrapper<int> wrapped(1);
|
||||
auto& ref = (wrapped = 9);
|
||||
CHECK(&ref == &wrapped);
|
||||
CHECK(static_cast<int>(wrapped) == 9);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
#include <doctest/doctest.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <hdk/grid/SharedPtrWrapper.hpp>
|
||||
|
||||
namespace {
|
||||
class IntWrapper final : public hdk::grid::SharedPtrWrapper<int> {
|
||||
public:
|
||||
using hdk::grid::SharedPtrWrapper<int>::DeleteFunc;
|
||||
using hdk::grid::SharedPtrWrapper<int>::SharedPtr;
|
||||
using hdk::grid::SharedPtrWrapper<int>::SharedPtrWrapper;
|
||||
|
||||
static SharedPtr View(int* ptr) { return get_or_view(ptr); }
|
||||
static SharedPtr Cache(int* ptr, DeleteFunc deleteFunc) { return get_or_cache(ptr, deleteFunc); }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("SharedPtrWrapper supports null and bool conversion") {
|
||||
IntWrapper wrapped(nullptr);
|
||||
CHECK_FALSE(static_cast<bool>(wrapped));
|
||||
|
||||
int* value = new int(11);
|
||||
int deletes = 0;
|
||||
IntWrapper owned(value, [&deletes](int* ptr) {
|
||||
++deletes;
|
||||
delete ptr;
|
||||
});
|
||||
|
||||
CHECK(static_cast<bool>(owned));
|
||||
CHECK(static_cast<int*>(owned) == value);
|
||||
|
||||
owned = nullptr;
|
||||
CHECK_FALSE(static_cast<bool>(owned));
|
||||
CHECK(deletes == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("SharedPtrWrapper copy and move preserve pointed object") {
|
||||
int deletes = 0;
|
||||
IntWrapper first(new int(8), [&deletes](int* ptr) {
|
||||
++deletes;
|
||||
delete ptr;
|
||||
});
|
||||
|
||||
IntWrapper copied(first);
|
||||
CHECK(static_cast<int*>(copied) == static_cast<int*>(first));
|
||||
|
||||
IntWrapper moved(std::move(copied));
|
||||
CHECK(static_cast<int*>(moved) == static_cast<int*>(first));
|
||||
|
||||
IntWrapper assigned(nullptr);
|
||||
assigned = first;
|
||||
CHECK(static_cast<int*>(assigned) == static_cast<int*>(first));
|
||||
|
||||
IntWrapper moveAssigned(nullptr);
|
||||
moveAssigned = std::move(assigned);
|
||||
CHECK(static_cast<int*>(moveAssigned) == static_cast<int*>(first));
|
||||
}
|
||||
|
||||
TEST_CASE("SharedPtrWrapper cache returns same control block") {
|
||||
int deletes = 0;
|
||||
int* raw = new int(3);
|
||||
auto deleter = [&deletes](int* ptr) {
|
||||
++deletes;
|
||||
delete ptr;
|
||||
};
|
||||
|
||||
auto cachedA = IntWrapper::Cache(raw, deleter);
|
||||
auto cachedB = IntWrapper::Cache(raw, deleter);
|
||||
|
||||
CHECK(cachedA.get() == raw);
|
||||
CHECK(cachedB.get() == raw);
|
||||
CHECK(cachedA.use_count() >= 2);
|
||||
|
||||
cachedA.reset();
|
||||
CHECK(deletes == 0);
|
||||
cachedB.reset();
|
||||
CHECK(deletes == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("SharedPtrWrapper view wraps pointers without ownership") {
|
||||
int stackValue = 5;
|
||||
auto viewA = IntWrapper::View(&stackValue);
|
||||
auto viewB = IntWrapper::View(&stackValue);
|
||||
|
||||
CHECK(viewA.get() == &stackValue);
|
||||
CHECK(viewB.get() == &stackValue);
|
||||
|
||||
int deletes = 0;
|
||||
int* raw = new int(13);
|
||||
IntWrapper owner(raw, [&deletes](int* ptr) {
|
||||
++deletes;
|
||||
delete ptr;
|
||||
});
|
||||
|
||||
auto cachedView = IntWrapper::View(raw);
|
||||
CHECK(cachedView.get() == raw);
|
||||
CHECK(cachedView.use_count() >= 2);
|
||||
|
||||
owner = nullptr;
|
||||
CHECK(deletes == 0);
|
||||
cachedView.reset();
|
||||
CHECK(deletes == 1);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include <doctest/doctest.h>
|
||||
Reference in New Issue
Block a user