Skip to content

Commit

Permalink
ComponentArena for creating/storing JUCE components (#517)
Browse files Browse the repository at this point in the history
* Setting up a Component Arenal

* SmallVector fixing when we call destructor

* Book-keeping for manual destruction

* Apply clang-format

* Silence clang-tidy warning

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
jatinchowdhury18 and github-actions[bot] authored Mar 28, 2024
1 parent f7e1f27 commit 8843e52
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#pragma once

namespace chowdsp
{
/**
* A wrapper for a pointer that will call the destructor
* for its underlying object when it goes out of scope,
* but WILL NOT free the underlying memory.
*
* This may be useful if the pointer is referring to some
* stack memory, or if the memory will be freed elsewhere,
* e.g. as part of a memory arena.
*/
template <typename T>
struct DestructiblePointer
{
DestructiblePointer() = default;
DestructiblePointer (T* ptr) : pointer (ptr) {} // NOLINT(google-explicit-constructor)
DestructiblePointer (const DestructiblePointer&) = delete;
DestructiblePointer& operator= (const DestructiblePointer&) = delete;
DestructiblePointer (DestructiblePointer&& other) noexcept : pointer (other.pointer)
{
other.pointer = nullptr;
}
DestructiblePointer& operator= (DestructiblePointer&& other) noexcept
{
if (this != &other)
{
destroy();
pointer = other.pointer;
other.pointer = nullptr;
}
return *this;
}

~DestructiblePointer()
{
destroy();
}

/**
* Calls the object destructor, and resets the pointer to null.
* Does nothing if the pointer is already null.
*/
void destroy()
{
if (pointer != nullptr)
pointer->~T();
pointer = nullptr;
}

/** Releases the pointer without destroying. */
T* release()
{
auto* ptr = pointer;
pointer = nullptr;
return pointer;
}

[[nodiscard]] T* get() { return pointer; }
[[nodiscard]] const T* get() const { return pointer; }

[[nodiscard]] T* operator->() { return get(); }
[[nodiscard]] const T* operator->() const { return get(); }
[[nodiscard]] T& operator*() { return *get(); }
[[nodiscard]] const T& operator*() const { return *get(); }

private:
T* pointer = nullptr;
};

template <typename T>
bool operator== (const DestructiblePointer<T>& p1, std::nullptr_t)
{
return p1.get() == nullptr;
}

template <typename T>
bool operator!= (const DestructiblePointer<T>& p1, std::nullptr_t)
{
return p1.get() != nullptr;
}

template <typename T>
bool operator== (std::nullptr_t, const DestructiblePointer<T>& p2)
{
return nullptr == p2.get();
}

template <typename T>
bool operator!= (std::nullptr_t, const DestructiblePointer<T>& p2)
{
return nullptr != p2.get();
}
} // namespace chowdsp
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class LocalPointer

reset();
pointer = new (data.data()) C (std::forward<Args> (args)...);
return reinterpret_cast<C*> (pointer);
return reinterpret_cast<C*> (pointer.get());
}

/**
Expand All @@ -81,21 +81,13 @@ class LocalPointer
{
if (pointer != nullptr)
{
// Why are we calling the destructor explicitly here?
// For some types it won't matter since we're zero-ing
// the local storage anyway, but if the type has something
// like a std::vector or std::unique_ptr inside it, or has
// some custom logic in its destructor, then we need to make
// sure that stuff gets taken care of before destroying the object.
pointer->~T();

pointer = nullptr;
pointer.destroy();
std::fill (std::begin (data), std::end (data), std::byte {});
}
}

[[nodiscard]] T* get() { return pointer; }
[[nodiscard]] const T* get() const { return pointer; }
[[nodiscard]] T* get() { return pointer.get(); }
[[nodiscard]] const T* get() const { return pointer.get(); }

[[nodiscard]] T* operator->() { return get(); }
[[nodiscard]] const T* operator->() const { return get(); }
Expand All @@ -104,7 +96,7 @@ class LocalPointer

private:
alignas (Alignment) std::array<std::byte, MaxSize> data {};
T* pointer = nullptr;
DestructiblePointer<T> pointer = nullptr;
};

template <typename T, size_t N, size_t A>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,15 @@ class SmallVector
void clear() noexcept
{
if (usingArray)
{
internal_needs_destruction = make_array<bool, head_size> (true);
for (size_t i = 0; i < internal_array_size_used; ++i)
{
internal_array[i].~T();
internal_needs_destruction[i] = false;
}
internal_array_size_used = 0;
}
internal_vector.clear();
}

Expand Down Expand Up @@ -443,7 +451,8 @@ class SmallVector
{
if (internal_array_size_used + 1 <= head_size)
{
internal_array[internal_array_size_used].~T();
if (internal_needs_destruction[internal_array_size_used])
internal_array[internal_array_size_used].~T();
new (&internal_array[internal_array_size_used]) T (args...);
internal_array_size_used++;
return internal_array[internal_array_size_used - 1];
Expand Down Expand Up @@ -517,6 +526,7 @@ class SmallVector

std::array<T, head_size> internal_array {};
size_t internal_array_size_used = 0;
std::array<bool, head_size> internal_needs_destruction = make_array<bool, head_size> (true);

std::vector<T> internal_vector {};

Expand Down
11 changes: 6 additions & 5 deletions modules/common/chowdsp_data_structures/chowdsp_data_structures.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ BEGIN_JUCE_MODULE_DECLARATION

#include "third_party/short_alloc.h"

#include "Helpers/chowdsp_ArrayHelpers.h"
#include "Helpers/chowdsp_TupleHelpers.h"
#include "Helpers/chowdsp_VectorHelpers.h"
#include "Helpers/chowdsp_Iterators.h"

#include "Structures/chowdsp_DoubleBuffer.h"
#include "Structures/chowdsp_DestructiblePointer.h"
#include "Structures/chowdsp_LocalPointer.h"
#include "Structures/chowdsp_OptionalPointer.h"
#include "Structures/chowdsp_RawObject.h"
#include "Structures/chowdsp_SmallVector.h"
#include "Structures/chowdsp_StringLiteral.h"

#include "Helpers/chowdsp_ArrayHelpers.h"
#include "Helpers/chowdsp_TupleHelpers.h"
#include "Helpers/chowdsp_VectorHelpers.h"
#include "Helpers/chowdsp_Iterators.h"

#include "Allocators/chowdsp_ArenaAllocator.h"
#include "Allocators/chowdsp_ChainedArenaAllocator.h"

Expand Down
67 changes: 67 additions & 0 deletions modules/gui/chowdsp_gui/Helpers/chowdsp_ComponentArena.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#pragma once

namespace chowdsp
{
/**
* A memory arena designed to be used with juce::Component objects
* (although other types can be allocated into the arena as well).
*/
template <size_t arena_chunk_size_bytes = 8192, size_t approx_component_count = 32>
class ComponentArena
{
public:
ComponentArena() = default;
ComponentArena (const ComponentArena&) = delete;
ComponentArena& operator= (const ComponentArena&) = delete;
ComponentArena (ComponentArena&&) noexcept = delete;
ComponentArena& operator= (ComponentArena&&) noexcept = delete;

~ComponentArena()
{
clear_all();
}

/** Allocates a single object into the arena. */
template <typename T, typename... Args>
T* allocate (Args&&... args)
{
auto* bytes = allocator.allocate_bytes (sizeof (T), alignof (T));
auto* new_component = new (bytes) T { std::forward<Args> (args)... };

if constexpr (std::is_base_of_v<juce::Component, T>)
component_list.emplace_back (new_component);

return new_component;
}

/** Allocates multiple objects into the arena. */
template <typename T, typename... Args>
nonstd::span<T> allocate_n (size_t n, Args&&... args)
{
auto* bytes = allocator.allocate_bytes (sizeof (T) * n, alignof (T));
auto span = nonstd::span<T> { reinterpret_cast<T*> (bytes), n };
for (auto& ptr : span)
{
auto* new_component = new (&ptr) T { std::forward<Args> (args)... };
if constexpr (std::is_base_of_v<juce::Component, T>)
component_list.emplace_back (new_component);
}
return span;
}

/**
* Reclaims the arena memory, and destroys any components
* that have been allocated into the arena.
*/
void clear_all()
{
component_list.clear();
allocator.clear();
}

ChainedArenaAllocator<std::array<std::byte, arena_chunk_size_bytes>> allocator {};

private:
SmallVector<DestructiblePointer<juce::Component>, approx_component_count> component_list {};
};
} // namespace chowdsp
4 changes: 3 additions & 1 deletion modules/gui/chowdsp_gui/chowdsp_gui.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
version: 2.1.0
name: ChowDSP GUI Utilities
description: Commonly used GUI utilities for ChowDSP plugins
dependencies: juce_core, juce_audio_processors, juce_gui_basics, chowdsp_core
dependencies: juce_core, juce_audio_processors, juce_gui_basics, chowdsp_core, chowdsp_data_structures
website: https://ccrma.stanford.edu/~jatin/chowdsp
license: GPLv3
Expand All @@ -33,6 +33,7 @@
#include <juce_gui_basics/juce_gui_basics.h>

#include <chowdsp_core/chowdsp_core.h>
#include <chowdsp_data_structures/chowdsp_data_structures.h>

#if JUCE_MODULE_AVAILABLE_juce_opengl && CHOWDSP_ENABLE_OPEN_GL_CONTEXT
#define CHOWDSP_OPENGL_IS_AVAILABLE 1
Expand All @@ -45,6 +46,7 @@

#include "LookAndFeel/chowdsp_ChowLNF.h"

#include "Helpers/chowdsp_ComponentArena.h"
#include "Helpers/chowdsp_LongPressActionHelper.h"
#include "Helpers/chowdsp_PopupMenuHelper.h"
#include "Helpers/chowdsp_OpenGLHelper.h"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,30 @@ TEST_CASE ("Small Vector Test", "[common][data-structures]")
REQUIRE (vec.empty());
}

SECTION ("Clear w/ Destruction Test")
{
chowdsp::SmallVector<std::shared_ptr<int>, 4> vec {
std::make_shared<int> (1),
std::make_shared<int> (2),
std::make_shared<int> (3),
};
vec.resize (3);
REQUIRE (*vec[2] == 3);

vec.clear();
REQUIRE (vec.empty());

vec.emplace_back (std::make_shared<int> (0));
vec.emplace_back (std::make_shared<int> (1));
vec.emplace_back (std::make_shared<int> (2));
vec.emplace_back (std::make_shared<int> (3));
vec.emplace_back (std::make_shared<int> (4));
REQUIRE (vec.size() == 5);

vec.clear();
REQUIRE (vec.empty());
}

SECTION ("Insert/Emplace Single Value Test")
{
chowdsp::SmallVector<double, 4> vec { 1.0, 2.0 };
Expand Down Expand Up @@ -409,7 +433,6 @@ TEST_CASE ("Small Vector Test", "[common][data-structures]")
vec.erase (vec.begin() + 1, vec.begin() + 3);
REQUIRE (vec.size() == 3);
REQUIRE (vec[0] == 1);
;
REQUIRE (vec[1] == 4);
REQUIRE (vec[2] == 5);

Expand Down
1 change: 1 addition & 0 deletions tests/gui_tests/chowdsp_gui_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ target_sources(chowdsp_gui_test PRIVATE
PopupMenuHelperTest.cpp
WindowInPluginTest.cpp
HostContextProviderTest.cpp
ComponentArenaTest.cpp

# OversamplingMenuTest.cpp
# PresetsCompTest.cpp
Expand Down
44 changes: 44 additions & 0 deletions tests/gui_tests/chowdsp_gui_test/ComponentArenaTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#include <CatchUtils.h>
#include <chowdsp_gui/chowdsp_gui.h>

TEST_CASE ("Component Arena Test", "[gui][data-structures]")
{
struct TestComponent : juce::Component
{
int& destructor_counter;
explicit TestComponent (int& dest_counter) : destructor_counter (dest_counter) {}
~TestComponent() override { ++destructor_counter; }
};

struct NotAComponent
{
int& destructor_counter;
explicit NotAComponent (int& dest_counter) : destructor_counter (dest_counter) {}
~NotAComponent() { ++destructor_counter; }
};

int destructor_counter = 0;
chowdsp::ComponentArena<> arena {};

SECTION ("Only allocate components")
{
arena.allocate<TestComponent> (destructor_counter);
arena.allocate_n<TestComponent> (4, destructor_counter);

REQUIRE (destructor_counter == 0);
arena.clear_all();
REQUIRE (destructor_counter == 5);
}

SECTION ("Allocate other types too")
{
arena.allocate<TestComponent> (destructor_counter);
arena.allocate<NotAComponent> (destructor_counter);
arena.allocate_n<TestComponent> (4, destructor_counter);
arena.allocate_n<NotAComponent> (3, destructor_counter);

REQUIRE (destructor_counter == 0);
arena.clear_all();
REQUIRE (destructor_counter == 5);
}
}

0 comments on commit 8843e52

Please sign in to comment.