Override Rack's Message class for prealloc is on stack, not on heap
Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
parent
a8009a172d
commit
c74923eb2d
3 changed files with 983 additions and 0 deletions
528
include/choc/choc_SmallVector.h
Normal file
528
include/choc/choc_SmallVector.h
Normal file
|
@ -0,0 +1,528 @@
|
||||||
|
//
|
||||||
|
// ██████ ██ ██ ██████ ██████
|
||||||
|
// ██ ██ ██ ██ ██ ██ ** Clean Header-Only Classes **
|
||||||
|
// ██ ███████ ██ ██ ██
|
||||||
|
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||||
|
// ██████ ██ ██ ██████ ██████
|
||||||
|
//
|
||||||
|
// CHOC is (C)2021 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||||
|
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||||
|
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||||
|
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
#ifndef CHOC_SMALLVECTOR_HEADER_INCLUDED
|
||||||
|
#define CHOC_SMALLVECTOR_HEADER_INCLUDED
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include "choc_Span.h"
|
||||||
|
|
||||||
|
namespace choc
|
||||||
|
{
|
||||||
|
|
||||||
|
/**
|
||||||
|
A std::vector-style container class, which uses some pre-allocated storage
|
||||||
|
to avoid heap allocation when the number of elements is small.
|
||||||
|
|
||||||
|
Inspired by LLVM's SmallVector, I've found this to be handy in many situations
|
||||||
|
where you know there's only likely to be a small or fixed number of elements,
|
||||||
|
and where performance is important.
|
||||||
|
|
||||||
|
It retains most of the same basic methods as std::vector, but without some of
|
||||||
|
the more exotic tricks that the std library uses, just to avoid things getting
|
||||||
|
too complicated.
|
||||||
|
*/
|
||||||
|
template <typename ElementType, size_t numPreallocatedElements>
|
||||||
|
struct SmallVector
|
||||||
|
{
|
||||||
|
using value_type = ElementType;
|
||||||
|
using reference = ElementType&;
|
||||||
|
using const_reference = const ElementType&;
|
||||||
|
using iterator = ElementType*;
|
||||||
|
using const_iterator = const ElementType*;
|
||||||
|
using size_type = size_t;
|
||||||
|
|
||||||
|
SmallVector() noexcept;
|
||||||
|
~SmallVector() noexcept;
|
||||||
|
|
||||||
|
SmallVector (SmallVector&&) noexcept;
|
||||||
|
SmallVector (const SmallVector&);
|
||||||
|
SmallVector& operator= (SmallVector&&) noexcept;
|
||||||
|
SmallVector& operator= (const SmallVector&);
|
||||||
|
|
||||||
|
/// Creates a SmallVector as a copy of some kind of iterable container.
|
||||||
|
template <typename VectorType>
|
||||||
|
SmallVector (const VectorType& initialContent);
|
||||||
|
|
||||||
|
/// Replaces the contents of this vector with a copy of some kind of iterable container.
|
||||||
|
template <typename VectorType>
|
||||||
|
SmallVector& operator= (const VectorType&);
|
||||||
|
|
||||||
|
reference operator[] (size_type index);
|
||||||
|
const_reference operator[] (size_type index) const;
|
||||||
|
|
||||||
|
value_type* data() const noexcept;
|
||||||
|
|
||||||
|
const_iterator begin() const noexcept;
|
||||||
|
const_iterator end() const noexcept;
|
||||||
|
const_iterator cbegin() const noexcept;
|
||||||
|
const_iterator cend() const noexcept;
|
||||||
|
iterator begin() noexcept;
|
||||||
|
iterator end() noexcept;
|
||||||
|
|
||||||
|
const_reference front() const;
|
||||||
|
reference front();
|
||||||
|
const_reference back() const;
|
||||||
|
reference back();
|
||||||
|
|
||||||
|
bool empty() const noexcept;
|
||||||
|
size_type size() const noexcept;
|
||||||
|
size_type length() const noexcept;
|
||||||
|
size_type capacity() const noexcept;
|
||||||
|
|
||||||
|
bool contains (const ElementType&) const;
|
||||||
|
|
||||||
|
void clear() noexcept;
|
||||||
|
void resize (size_type newSize);
|
||||||
|
void reserve (size_type requiredNumElements);
|
||||||
|
|
||||||
|
void push_back (const value_type&);
|
||||||
|
void push_back (value_type&&);
|
||||||
|
|
||||||
|
/// Handy method to add multiple elements with a single push_back call.
|
||||||
|
template <typename... Others>
|
||||||
|
void push_back (const value_type& first, Others&&... others);
|
||||||
|
|
||||||
|
template <typename... ConstructorArgs>
|
||||||
|
void emplace_back (ConstructorArgs&&... args);
|
||||||
|
|
||||||
|
void pop_back();
|
||||||
|
|
||||||
|
void insert (iterator insertPosition, const value_type& valueToInsert);
|
||||||
|
void insert (iterator insertPosition, value_type&& valueToInsert);
|
||||||
|
|
||||||
|
void erase (iterator startPosition);
|
||||||
|
void erase (iterator startPosition, iterator endPosition);
|
||||||
|
|
||||||
|
bool operator== (span<value_type>) const;
|
||||||
|
bool operator!= (span<value_type>) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
value_type* elements;
|
||||||
|
size_type numElements = 0, numAllocated = numPreallocatedElements;
|
||||||
|
uint64_t internalStorage[(numPreallocatedElements * sizeof (value_type) + sizeof (uint64_t) - 1) / sizeof (uint64_t)];
|
||||||
|
|
||||||
|
void shrink (size_type);
|
||||||
|
value_type* getInternalStorage() noexcept { return reinterpret_cast<value_type*> (internalStorage); }
|
||||||
|
bool isUsingInternalStorage() const noexcept { return numAllocated <= numPreallocatedElements; }
|
||||||
|
void resetToInternalStorage() noexcept;
|
||||||
|
void freeHeapAndResetToInternalStorage() noexcept;
|
||||||
|
|
||||||
|
static inline ElementType& _nullValue() noexcept { static ElementType e = {}; return e; }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
// _ _ _ _
|
||||||
|
// __| | ___ | |_ __ _ (_)| | ___
|
||||||
|
// / _` | / _ \| __| / _` || || |/ __|
|
||||||
|
// | (_| || __/| |_ | (_| || || |\__ \ _ _ _
|
||||||
|
// \__,_| \___| \__| \__,_||_||_||___/(_)(_)(_)
|
||||||
|
//
|
||||||
|
// Code beyond this point is implementation detail...
|
||||||
|
//
|
||||||
|
//==============================================================================
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
SmallVector<ElementType, preSize>::SmallVector() noexcept : elements (getInternalStorage())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
SmallVector<ElementType, preSize>::~SmallVector() noexcept
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
SmallVector<ElementType, preSize>::SmallVector (const SmallVector& other) : SmallVector()
|
||||||
|
{
|
||||||
|
operator= (other);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
template <typename VectorType>
|
||||||
|
SmallVector<ElementType, preSize>::SmallVector (const VectorType& initialContent) : SmallVector()
|
||||||
|
{
|
||||||
|
reserve (initialContent.size());
|
||||||
|
|
||||||
|
for (auto& i : initialContent)
|
||||||
|
emplace_back (i);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
SmallVector<ElementType, preSize>::SmallVector (SmallVector&& other) noexcept
|
||||||
|
{
|
||||||
|
if (other.isUsingInternalStorage())
|
||||||
|
{
|
||||||
|
elements = getInternalStorage();
|
||||||
|
numElements = other.numElements;
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
new (elements + i) value_type (std::move (other.elements[i]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
elements = other.elements;
|
||||||
|
numElements = other.numElements;
|
||||||
|
numAllocated = other.numAllocated;
|
||||||
|
other.resetToInternalStorage();
|
||||||
|
other.numElements = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
SmallVector<ElementType, preSize>& SmallVector<ElementType, preSize>::operator= (SmallVector&& other) noexcept
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
|
||||||
|
if (other.isUsingInternalStorage())
|
||||||
|
{
|
||||||
|
numElements = other.numElements;
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
new (elements + i) value_type (std::move (other.elements[i]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
elements = other.elements;
|
||||||
|
numElements = other.numElements;
|
||||||
|
numAllocated = other.numAllocated;
|
||||||
|
other.resetToInternalStorage();
|
||||||
|
other.numElements = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
SmallVector<ElementType, preSize>& SmallVector<ElementType, preSize>::operator= (const SmallVector& other)
|
||||||
|
{
|
||||||
|
if (other.size() > numElements)
|
||||||
|
{
|
||||||
|
reserve (other.size());
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
elements[i] = other.elements[i];
|
||||||
|
|
||||||
|
for (size_type i = numElements; i < other.size(); ++i)
|
||||||
|
new (elements + i) value_type (other.elements[i]);
|
||||||
|
|
||||||
|
numElements = other.size();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shrink (other.size());
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
elements[i] = other.elements[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
template <typename VectorType>
|
||||||
|
SmallVector<ElementType, preSize>& SmallVector<ElementType, preSize>::operator= (const VectorType& other)
|
||||||
|
{
|
||||||
|
if (other.size() > numElements)
|
||||||
|
{
|
||||||
|
reserve (other.size());
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
elements[i] = other[i];
|
||||||
|
|
||||||
|
for (size_type i = numElements; i < other.size(); ++i)
|
||||||
|
new (elements + i) value_type (other[i]);
|
||||||
|
|
||||||
|
numElements = other.size();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shrink (other.size());
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
elements[i] = other[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::resetToInternalStorage() noexcept
|
||||||
|
{
|
||||||
|
elements = getInternalStorage();
|
||||||
|
numAllocated = preSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::freeHeapAndResetToInternalStorage() noexcept
|
||||||
|
{
|
||||||
|
if (! isUsingInternalStorage())
|
||||||
|
{
|
||||||
|
delete[] reinterpret_cast<char*> (elements);
|
||||||
|
resetToInternalStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::reference SmallVector<ElementType, preSize>::operator[] (size_type index)
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (index < numElements, _nullValue());
|
||||||
|
return elements[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_reference SmallVector<ElementType, preSize>::operator[] (size_type index) const
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (index < numElements, _nullValue());
|
||||||
|
return elements[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::value_type* SmallVector<ElementType, preSize>::data() const noexcept { return elements; }
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_iterator SmallVector<ElementType, preSize>::begin() const noexcept { return elements; }
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_iterator SmallVector<ElementType, preSize>::end() const noexcept { return elements + numElements; }
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_iterator SmallVector<ElementType, preSize>::cbegin() const noexcept { return elements; }
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_iterator SmallVector<ElementType, preSize>::cend() const noexcept { return elements + numElements; }
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::iterator SmallVector<ElementType, preSize>::begin() noexcept { return elements; }
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::iterator SmallVector<ElementType, preSize>::end() noexcept { return elements + numElements; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::reference SmallVector<ElementType, preSize>::front()
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue());
|
||||||
|
return elements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_reference SmallVector<ElementType, preSize>::front() const
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue());
|
||||||
|
return elements[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::reference SmallVector<ElementType, preSize>::back()
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue());
|
||||||
|
return elements[numElements - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::const_reference SmallVector<ElementType, preSize>::back() const
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue());
|
||||||
|
return elements[numElements - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::size_type SmallVector<ElementType, preSize>::size() const noexcept { return numElements; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::size_type SmallVector<ElementType, preSize>::length() const noexcept { return numElements; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
typename SmallVector<ElementType, preSize>::size_type SmallVector<ElementType, preSize>::capacity() const noexcept { return numAllocated; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
bool SmallVector<ElementType, preSize>::empty() const noexcept { return numElements == 0; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
bool SmallVector<ElementType, preSize>::contains (const ElementType& target) const
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < numElements; ++i)
|
||||||
|
if (elements[i] == target)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
bool SmallVector<ElementType, preSize>::operator== (span<value_type> other) const { return span<value_type> (*this) == other; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
bool SmallVector<ElementType, preSize>::operator!= (span<value_type> other) const { return span<value_type> (*this) != other; }
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::push_back (const value_type& item)
|
||||||
|
{
|
||||||
|
reserve (numElements + 1);
|
||||||
|
new (elements + numElements) value_type (item);
|
||||||
|
++numElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::push_back (value_type&& item)
|
||||||
|
{
|
||||||
|
reserve (numElements + 1);
|
||||||
|
new (elements + numElements) value_type (std::move (item));
|
||||||
|
++numElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
template <typename... Others>
|
||||||
|
void SmallVector<ElementType, preSize>::push_back (const value_type& first, Others&&... others)
|
||||||
|
{
|
||||||
|
reserve (numElements + 1 + sizeof... (others));
|
||||||
|
push_back (first);
|
||||||
|
push_back (std::forward<Others> (others)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
template <typename... ConstructorArgs>
|
||||||
|
void SmallVector<ElementType, preSize>::emplace_back (ConstructorArgs&&... args)
|
||||||
|
{
|
||||||
|
reserve (numElements + 1);
|
||||||
|
new (elements + numElements) value_type (std::forward<ConstructorArgs> (args)...);
|
||||||
|
++numElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::insert (iterator insertPos, const value_type& item)
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (insertPos != nullptr && insertPos >= begin() && insertPos <= end(),);
|
||||||
|
auto index = insertPos - begin();
|
||||||
|
push_back (item);
|
||||||
|
std::rotate (begin() + index, end() - 1, end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::insert (iterator insertPos, value_type&& item)
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (insertPos != nullptr && insertPos >= begin() && insertPos <= end(),);
|
||||||
|
auto index = insertPos - begin();
|
||||||
|
push_back (std::move (item));
|
||||||
|
std::rotate (begin() + index, end() - 1, end());
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::pop_back()
|
||||||
|
{
|
||||||
|
if (numElements == 1)
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (numElements > 0,);
|
||||||
|
elements[--numElements].~value_type();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::clear() noexcept
|
||||||
|
{
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
elements[i].~value_type();
|
||||||
|
|
||||||
|
numElements = 0;
|
||||||
|
freeHeapAndResetToInternalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::resize (size_type newSize)
|
||||||
|
{
|
||||||
|
if (newSize > numElements)
|
||||||
|
{
|
||||||
|
reserve (newSize);
|
||||||
|
|
||||||
|
while (numElements < newSize)
|
||||||
|
new (elements + numElements++) value_type (value_type());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
shrink (newSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::shrink (size_type newSize)
|
||||||
|
{
|
||||||
|
if (newSize == 0)
|
||||||
|
return clear();
|
||||||
|
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (newSize <= numElements,);
|
||||||
|
|
||||||
|
while (newSize < numElements && numElements > 0)
|
||||||
|
elements[--numElements].~value_type();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::reserve (size_type requiredNumElements)
|
||||||
|
{
|
||||||
|
if (requiredNumElements > numAllocated)
|
||||||
|
{
|
||||||
|
requiredNumElements = static_cast<size_type> ((requiredNumElements + 15u) & ~(size_type) 15u);
|
||||||
|
|
||||||
|
if (requiredNumElements > preSize)
|
||||||
|
{
|
||||||
|
auto* newBuffer = reinterpret_cast<value_type*> (new char[requiredNumElements * sizeof (value_type)]);
|
||||||
|
|
||||||
|
for (size_type i = 0; i < numElements; ++i)
|
||||||
|
{
|
||||||
|
new (newBuffer + i) value_type (std::move (elements[i]));
|
||||||
|
elements[i].~value_type();
|
||||||
|
}
|
||||||
|
|
||||||
|
freeHeapAndResetToInternalStorage();
|
||||||
|
elements = newBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
numAllocated = requiredNumElements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::erase (iterator startElement)
|
||||||
|
{
|
||||||
|
erase (startElement, startElement + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename ElementType, size_t preSize>
|
||||||
|
void SmallVector<ElementType, preSize>::erase (iterator startElement, iterator endElement)
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (startElement != nullptr && startElement >= begin() && startElement <= end(),);
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (endElement != nullptr && endElement >= begin() && endElement <= end(),);
|
||||||
|
|
||||||
|
if (startElement != endElement)
|
||||||
|
{
|
||||||
|
DISTRHO_SAFE_ASSERT_RETURN (startElement < endElement,);
|
||||||
|
|
||||||
|
if (endElement == end())
|
||||||
|
return shrink (static_cast<size_type> (startElement - begin()));
|
||||||
|
|
||||||
|
auto dest = startElement;
|
||||||
|
|
||||||
|
for (auto src = endElement; src < end(); ++dest, ++src)
|
||||||
|
*dest = std::move (*src);
|
||||||
|
|
||||||
|
shrink (size() - static_cast<size_type> (endElement - startElement));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CHOC_SMALLVECTOR_HEADER_INCLUDED
|
127
include/choc/choc_Span.h
Normal file
127
include/choc/choc_Span.h
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
//
|
||||||
|
// ██████ ██ ██ ██████ ██████
|
||||||
|
// ██ ██ ██ ██ ██ ██ ** Clean Header-Only Classes **
|
||||||
|
// ██ ███████ ██ ██ ██
|
||||||
|
// ██ ██ ██ ██ ██ ██ https://github.com/Tracktion/choc
|
||||||
|
// ██████ ██ ██ ██████ ██████
|
||||||
|
//
|
||||||
|
// CHOC is (C)2021 Tracktion Corporation, and is offered under the terms of the ISC license:
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any purpose with or
|
||||||
|
// without fee is hereby granted, provided that the above copyright notice and this permission
|
||||||
|
// notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
||||||
|
// WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
||||||
|
// CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||||
|
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
#ifndef CHOC_SPAN_HEADER_INCLUDED
|
||||||
|
#define CHOC_SPAN_HEADER_INCLUDED
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include "DistrhoUtils.hpp"
|
||||||
|
|
||||||
|
namespace choc
|
||||||
|
{
|
||||||
|
|
||||||
|
//==============================================================================
|
||||||
|
/** This is a temporary stunt-double for std::span, with the intention of it being
|
||||||
|
deprecated when there's more widespread compiler support for the real std::span.
|
||||||
|
|
||||||
|
This class has fewer bells-and-whistles than a real std::span, but it does have
|
||||||
|
the advantage of calling CHOC_ASSERT when mistakes are made like out-of-range
|
||||||
|
accesses, which can be useful for getting clean error handling rather than UB.
|
||||||
|
*/
|
||||||
|
template <typename Item>
|
||||||
|
struct span
|
||||||
|
{
|
||||||
|
span() = default;
|
||||||
|
span (const span&) = default;
|
||||||
|
span (span&&) = default;
|
||||||
|
span& operator= (span&&) = default;
|
||||||
|
span& operator= (const span&) = default;
|
||||||
|
|
||||||
|
/// Construct from some raw start and end pointers. For an empty span, these
|
||||||
|
/// can both be nullptr, but if one is a real pointer then the caller must ensure
|
||||||
|
/// that start <= end.
|
||||||
|
span (Item* start, Item* end) noexcept : s (start), e (end) {}
|
||||||
|
|
||||||
|
/// Constructs a span from a pointer and length.
|
||||||
|
/// The pointer must not be nullptr unless the length is 0.
|
||||||
|
span (const Item* start, size_t length) noexcept : span (const_cast<Item*> (start), const_cast<Item*> (start) + length) {}
|
||||||
|
|
||||||
|
/// Constructor taking a raw C++ array.
|
||||||
|
template <size_t length>
|
||||||
|
span (Item (&array)[length]) : span (array, length) {}
|
||||||
|
|
||||||
|
/// Constructor which takes some kind of class like std::vector or std::array.
|
||||||
|
/// Any class that provides data() and size() methods can be passed in.
|
||||||
|
template <typename VectorOrArray>
|
||||||
|
span (const VectorOrArray& v) : span (v.data(), v.size()) {}
|
||||||
|
|
||||||
|
/// Returns true if the span is empty.
|
||||||
|
bool empty() const { return s == e; }
|
||||||
|
|
||||||
|
/// Returns the number of elements.
|
||||||
|
/// The length() and size() methods are equivalent.
|
||||||
|
size_t size() const { return static_cast<size_t> (e - s); }
|
||||||
|
|
||||||
|
/// Returns the number of elements.
|
||||||
|
/// The length() and size() methods are equivalent.
|
||||||
|
size_t length() const { return static_cast<size_t> (e - s); }
|
||||||
|
|
||||||
|
/// Returns a raw pointer to the start of the data.
|
||||||
|
Item* data() const noexcept { return s; }
|
||||||
|
|
||||||
|
const Item& front() const { DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue()); return *s; }
|
||||||
|
const Item& back() const { DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue()); return *(e - 1); }
|
||||||
|
Item& front() { DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue()); return *s; }
|
||||||
|
Item& back() { DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue()); return *(e - 1); }
|
||||||
|
|
||||||
|
const Item& operator[] (size_t index) const { DISTRHO_SAFE_ASSERT_RETURN (index < length(), _nullValue()); return s[index]; }
|
||||||
|
Item& operator[] (size_t index) { DISTRHO_SAFE_ASSERT_RETURN (index < length(), _nullValue()); return s[index]; }
|
||||||
|
|
||||||
|
/// A handy bonus function for getting a (non-empty) span's tail elements
|
||||||
|
span tail() const { DISTRHO_SAFE_ASSERT_RETURN (! empty(), _nullValue()); return { s + 1, e }; }
|
||||||
|
|
||||||
|
const Item* begin() const noexcept { return s; }
|
||||||
|
const Item* end() const noexcept { return e; }
|
||||||
|
Item* begin() noexcept { return s; }
|
||||||
|
Item* end() noexcept { return e; }
|
||||||
|
|
||||||
|
/// Helper function to return a std::vector copy of the span's elements.
|
||||||
|
std::vector<typename std::remove_const<Item>::type> createVector() const
|
||||||
|
{
|
||||||
|
return std::vector<typename std::remove_const<Item>::type> (s, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Two spans are considered identical if their elements are all comparable
|
||||||
|
template <typename OtherSpan>
|
||||||
|
bool operator== (const OtherSpan& other) const
|
||||||
|
{
|
||||||
|
auto sz = size();
|
||||||
|
|
||||||
|
if (sz != other.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (decltype (sz) i = 0; i < sz; ++i)
|
||||||
|
if (s[i] != other.s[i])
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename OtherSpan>
|
||||||
|
bool operator!= (const OtherSpan& other) const { return ! operator== (other); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Item* s = {};
|
||||||
|
Item* e = {};
|
||||||
|
|
||||||
|
static inline Item& _nullValue() noexcept { static Item e = {}; return e; }
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace choc
|
||||||
|
|
||||||
|
#endif // CHOC_SPAN_HEADER_INCLUDED
|
328
include/midi.hpp
Normal file
328
include/midi.hpp
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
/*
|
||||||
|
* DISTRHO Cardinal Plugin
|
||||||
|
* Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License as
|
||||||
|
* published by the Free Software Foundation; either version 3 of
|
||||||
|
* the License, or any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* For a full copy of the GNU General Public License see the LICENSE file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is an edited version of VCVRack's midi.hpp
|
||||||
|
* Copyright (C) 2016-2021 VCV.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License as
|
||||||
|
* published by the Free Software Foundation; either version 3 of
|
||||||
|
* the License, or (at your option) any later version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
#include <jansson.h>
|
||||||
|
|
||||||
|
#include <common.hpp>
|
||||||
|
#include <context.hpp>
|
||||||
|
|
||||||
|
#include "choc/choc_SmallVector.h"
|
||||||
|
|
||||||
|
namespace rack {
|
||||||
|
/** Abstraction for all MIDI drivers in Rack */
|
||||||
|
namespace midi {
|
||||||
|
|
||||||
|
|
||||||
|
struct Message {
|
||||||
|
/** Initialized to 3 empty bytes. */
|
||||||
|
choc::SmallVector<uint8_t, 3> bytes;
|
||||||
|
/** The Engine frame timestamp of the Message.
|
||||||
|
For output messages, the frame when the message was generated.
|
||||||
|
For input messages, the frame when it is intended to be processed.
|
||||||
|
-1 for undefined, to be sent or processed immediately.
|
||||||
|
*/
|
||||||
|
int64_t frame = -1;
|
||||||
|
|
||||||
|
Message() {}
|
||||||
|
|
||||||
|
int getSize() const {
|
||||||
|
return bytes.size();
|
||||||
|
}
|
||||||
|
void setSize(int size) {
|
||||||
|
bytes.resize(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getChannel() const {
|
||||||
|
if (bytes.size() < 1)
|
||||||
|
return 0;
|
||||||
|
return bytes[0] & 0xf;
|
||||||
|
}
|
||||||
|
void setChannel(uint8_t channel) {
|
||||||
|
if (bytes.size() < 1)
|
||||||
|
return;
|
||||||
|
bytes[0] = (bytes[0] & 0xf0) | (channel & 0xf);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getStatus() const {
|
||||||
|
if (bytes.size() < 1)
|
||||||
|
return 0;
|
||||||
|
return bytes[0] >> 4;
|
||||||
|
}
|
||||||
|
void setStatus(uint8_t status) {
|
||||||
|
if (bytes.size() < 1)
|
||||||
|
return;
|
||||||
|
bytes[0] = (bytes[0] & 0xf) | (status << 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getNote() const {
|
||||||
|
if (bytes.size() < 2)
|
||||||
|
return 0;
|
||||||
|
return bytes[1];
|
||||||
|
}
|
||||||
|
void setNote(uint8_t note) {
|
||||||
|
if (bytes.size() < 2)
|
||||||
|
return;
|
||||||
|
bytes[1] = note & 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getValue() const {
|
||||||
|
if (bytes.size() < 3)
|
||||||
|
return 0;
|
||||||
|
return bytes[2];
|
||||||
|
}
|
||||||
|
void setValue(uint8_t value) {
|
||||||
|
if (bytes.size() < 3)
|
||||||
|
return;
|
||||||
|
bytes[2] = value & 0x7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toString() const;
|
||||||
|
|
||||||
|
int64_t getFrame() const {
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFrame(int64_t frame) {
|
||||||
|
this->frame = frame;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// Driver
|
||||||
|
////////////////////
|
||||||
|
|
||||||
|
struct InputDevice;
|
||||||
|
struct Input;
|
||||||
|
struct OutputDevice;
|
||||||
|
struct Output;
|
||||||
|
|
||||||
|
/** Wraps a MIDI driver API containing any number of MIDI devices.
|
||||||
|
*/
|
||||||
|
struct Driver {
|
||||||
|
virtual ~Driver() {}
|
||||||
|
/** Returns the name of the driver. E.g. "ALSA". */
|
||||||
|
virtual std::string getName() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
/** Returns a list of all input device IDs that can be subscribed to. */
|
||||||
|
virtual std::vector<int> getInputDeviceIds() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
/** Returns the default device to use when the driver is selected, or -1 for none. */
|
||||||
|
virtual int getDefaultInputDeviceId() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
/** Returns the name of an input device without obtaining it. */
|
||||||
|
virtual std::string getInputDeviceName(int deviceId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
/** Adds the given port as a reference holder of a device and returns the it.
|
||||||
|
Creates the Device if no ports are subscribed before calling.
|
||||||
|
*/
|
||||||
|
virtual InputDevice* subscribeInput(int deviceId, Input* input) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
/** Removes the give port as a reference holder of a device.
|
||||||
|
Deletes the Device if no ports are subscribed after calling.
|
||||||
|
*/
|
||||||
|
virtual void unsubscribeInput(int deviceId, Input* input) {}
|
||||||
|
|
||||||
|
// The following behave identically as the above methods except for outputs.
|
||||||
|
|
||||||
|
virtual std::vector<int> getOutputDeviceIds() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
virtual int getDefaultOutputDeviceId() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
virtual std::string getOutputDeviceName(int deviceId) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
virtual OutputDevice* subscribeOutput(int deviceId, Output* output) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
virtual void unsubscribeOutput(int deviceId, Output* output) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// Device
|
||||||
|
////////////////////
|
||||||
|
|
||||||
|
/** A single MIDI device of a driver API.
|
||||||
|
|
||||||
|
Modules and the UI should not interact with this API directly. Use Port instead.
|
||||||
|
|
||||||
|
Methods throw `rack::Exception` if the driver API has an exception.
|
||||||
|
*/
|
||||||
|
struct Device {
|
||||||
|
virtual ~Device() {}
|
||||||
|
virtual std::string getName() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputDevice : Device {
|
||||||
|
std::set<Input*> subscribed;
|
||||||
|
/** Not public. Use Driver::subscribeInput(). */
|
||||||
|
void subscribe(Input* input);
|
||||||
|
/** Not public. Use Driver::unsubscribeInput(). */
|
||||||
|
void unsubscribe(Input* input);
|
||||||
|
/** Called when a MIDI message is received from the device. */
|
||||||
|
void onMessage(const Message& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputDevice : Device {
|
||||||
|
std::set<Output*> subscribed;
|
||||||
|
/** Not public. Use Driver::subscribeOutput(). */
|
||||||
|
void subscribe(Output* output);
|
||||||
|
/** Not public. Use Driver::unsubscribeOutput(). */
|
||||||
|
void unsubscribe(Output* output);
|
||||||
|
/** Sends a MIDI message to the device. */
|
||||||
|
virtual void sendMessage(const Message& message) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
////////////////////
|
||||||
|
// Port
|
||||||
|
////////////////////
|
||||||
|
|
||||||
|
/** A handle to a Device, typically owned by modules to have shared access to a single Device.
|
||||||
|
|
||||||
|
All Port methods safely wrap Drivers methods.
|
||||||
|
That is, if the active Device throws a `rack::Exception`, it is caught and logged inside all Port methods, so they do not throw exceptions.
|
||||||
|
|
||||||
|
Use Input or Output subclasses in your module, not Port directly.
|
||||||
|
*/
|
||||||
|
struct Port {
|
||||||
|
/** For MIDI output, the channel to automatically set outbound messages.
|
||||||
|
If -1, the channel is not overwritten and must be set by MIDI generator.
|
||||||
|
|
||||||
|
For MIDI input, messages will be filtered by the channel.
|
||||||
|
If -1, all MIDI channels pass through.
|
||||||
|
*/
|
||||||
|
int channel = -1;
|
||||||
|
|
||||||
|
// private
|
||||||
|
int driverId = -1;
|
||||||
|
int deviceId = -1;
|
||||||
|
/** Not owned */
|
||||||
|
Driver* driver = NULL;
|
||||||
|
Device* device = NULL;
|
||||||
|
Context* context;
|
||||||
|
|
||||||
|
Port();
|
||||||
|
virtual ~Port();
|
||||||
|
|
||||||
|
Driver* getDriver();
|
||||||
|
int getDriverId();
|
||||||
|
void setDriverId(int driverId);
|
||||||
|
|
||||||
|
Device* getDevice();
|
||||||
|
virtual std::vector<int> getDeviceIds() = 0;
|
||||||
|
virtual int getDefaultDeviceId() = 0;
|
||||||
|
int getDeviceId();
|
||||||
|
virtual void setDeviceId(int deviceId) = 0;
|
||||||
|
virtual std::string getDeviceName(int deviceId) = 0;
|
||||||
|
|
||||||
|
virtual std::vector<int> getChannels() = 0;
|
||||||
|
int getChannel();
|
||||||
|
void setChannel(int channel);
|
||||||
|
std::string getChannelName(int channel);
|
||||||
|
|
||||||
|
json_t* toJson();
|
||||||
|
void fromJson(json_t* rootJ);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct Input : Port {
|
||||||
|
/** Not owned */
|
||||||
|
InputDevice* inputDevice = NULL;
|
||||||
|
|
||||||
|
Input();
|
||||||
|
~Input();
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
std::vector<int> getDeviceIds() override;
|
||||||
|
int getDefaultDeviceId() override;
|
||||||
|
void setDeviceId(int deviceId) override;
|
||||||
|
std::string getDeviceName(int deviceId) override;
|
||||||
|
|
||||||
|
std::vector<int> getChannels() override;
|
||||||
|
|
||||||
|
virtual void onMessage(const Message& message) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** An Input port that stores incoming MIDI messages and releases them when ready according to their frame timestamp.
|
||||||
|
*/
|
||||||
|
struct InputQueue : Input {
|
||||||
|
struct Internal;
|
||||||
|
Internal* internal;
|
||||||
|
|
||||||
|
InputQueue();
|
||||||
|
~InputQueue();
|
||||||
|
void onMessage(const Message& message) override;
|
||||||
|
/** Pops and returns the next message (by setting `messageOut`) if its frame timestamp is `maxFrame` or earlier.
|
||||||
|
Returns whether a message was returned.
|
||||||
|
*/
|
||||||
|
bool tryPop(Message* messageOut, int64_t maxFrame);
|
||||||
|
size_t size();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct Output : Port {
|
||||||
|
/** Not owned */
|
||||||
|
OutputDevice* outputDevice = NULL;
|
||||||
|
|
||||||
|
Output();
|
||||||
|
~Output();
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
std::vector<int> getDeviceIds() override;
|
||||||
|
int getDefaultDeviceId() override;
|
||||||
|
void setDeviceId(int deviceId) override;
|
||||||
|
std::string getDeviceName(int deviceId) override;
|
||||||
|
|
||||||
|
std::vector<int> getChannels() override;
|
||||||
|
|
||||||
|
void sendMessage(const Message& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
PRIVATE void init();
|
||||||
|
PRIVATE void destroy();
|
||||||
|
/** Registers a new MIDI driver. Takes pointer ownership. */
|
||||||
|
void addDriver(int driverId, Driver* driver);
|
||||||
|
std::vector<int> getDriverIds();
|
||||||
|
Driver* getDriver(int driverId);
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace midi
|
||||||
|
} // namespace rack
|
Loading…
Add table
Add a link
Reference in a new issue