aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-05-05 14:27:20 +0200
committerStefan Boberg <[email protected]>2026-05-05 14:27:20 +0200
commit5fc88d696911da2ff97275b5f5dc7d10775696ba (patch)
tree3346cb3a661bb1f2ab5a0794135a612dfdd2238d /src
parentwatchdog ephemeral port exhaust (#1022) (diff)
downloadarchived-zen-sb/shared-string.tar.xz
archived-zen-sb/shared-string.zip
zencore: add SharedString with intrusive atomic refcountsb/shared-string
Mirrors CompactString's compact length-prefix layout but stores an atomic<uint32_t> in the buffer header so multiple instances can share a single allocation. Copies just bump the refcount; the buffer is freed when the last referencing instance is destroyed.
Diffstat (limited to 'src')
-rw-r--r--src/zencore/include/zencore/string.h135
-rw-r--r--src/zencore/string.cpp144
2 files changed, 279 insertions, 0 deletions
diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h
index 53bfd196d..b62f4a4d1 100644
--- a/src/zencore/include/zencore/string.h
+++ b/src/zencore/include/zencore/string.h
@@ -7,6 +7,7 @@
#include <stdint.h>
#include <string.h>
+#include <atomic>
#include <charconv>
#include <compare>
#include <concepts>
@@ -1465,6 +1466,140 @@ private:
//////////////////////////////////////////////////////////////////////////
+/// Owns a heap-allocated, null-terminated string buffer that can be cheaply
+/// shared between instances via an intrusive atomic reference count stored
+/// in the buffer header. Copying a SharedString just bumps the refcount;
+/// the underlying bytes are released when the last referencing instance is
+/// destroyed.
+///
+/// The buffer layout is [refcount][length-prefix][chars...][NUL]. The
+/// length-prefix scheme matches CompactString: a byte below 0xFF stores the
+/// literal length, while 0xFF is a sentinel meaning "length >= 255",
+/// recovered via strlen() past the prefix.
+///
+/// Empty strings carry no allocation; the default-constructed state is the
+/// canonical empty SharedString, and constructing from an empty view leaves
+/// the header pointer null.
+class SharedString
+{
+public:
+ SharedString() = default;
+
+ explicit SharedString(std::string_view Str)
+ {
+ if (Str.empty())
+ {
+ return;
+ }
+ m_Header = AllocateBuffer(Str);
+ }
+
+ ~SharedString() { ReleaseBuffer(m_Header); }
+
+ SharedString(const SharedString& Other) noexcept : m_Header(Other.m_Header)
+ {
+ if (m_Header)
+ {
+ m_Header->RefCount.fetch_add(1, std::memory_order_relaxed);
+ }
+ }
+
+ SharedString& operator=(const SharedString& Other) noexcept
+ {
+ if (m_Header != Other.m_Header)
+ {
+ if (Other.m_Header)
+ {
+ Other.m_Header->RefCount.fetch_add(1, std::memory_order_relaxed);
+ }
+ ReleaseBuffer(m_Header);
+ m_Header = Other.m_Header;
+ }
+ return *this;
+ }
+
+ SharedString(SharedString&& Other) noexcept : m_Header(Other.m_Header) { Other.m_Header = nullptr; }
+
+ SharedString& operator=(SharedString&& Other) noexcept
+ {
+ if (this != &Other)
+ {
+ ReleaseBuffer(m_Header);
+ m_Header = Other.m_Header;
+ Other.m_Header = nullptr;
+ }
+ return *this;
+ }
+
+ const char* c_str() const { return m_Header ? Chars(m_Header) : ""; }
+
+ size_t Size() const
+ {
+ if (!m_Header)
+ {
+ return 0;
+ }
+ const uint8_t Prefix = static_cast<uint8_t>(*PrefixPtr(m_Header));
+ return Prefix < LengthFallbackSentinel ? Prefix : LengthFallbackSentinel + strlen(Chars(m_Header) + LengthFallbackSentinel);
+ }
+
+ std::string_view ToView() const { return {c_str(), Size()}; }
+ bool IsEmpty() const { return m_Header == nullptr; }
+
+ /// Number of SharedString instances currently sharing this buffer.
+ /// Returns 0 for the empty state. Intended for tests and diagnostics.
+ uint32_t UseCount() const { return m_Header ? m_Header->RefCount.load(std::memory_order_relaxed) : 0; }
+
+ operator std::string_view() const { return ToView(); }
+
+private:
+ static constexpr uint8_t LengthFallbackSentinel = 0xFF;
+
+ struct Header
+ {
+ std::atomic<uint32_t> RefCount;
+ };
+
+ static_assert(sizeof(Header) == sizeof(uint32_t), "SharedString::Header must be a single 32-bit refcount with no padding");
+
+ Header* m_Header = nullptr;
+
+ static char* PrefixPtr(Header* H) { return reinterpret_cast<char*>(H) + sizeof(Header); }
+ static const char* PrefixPtr(const Header* H) { return reinterpret_cast<const char*>(H) + sizeof(Header); }
+ static const char* Chars(const Header* H) { return PrefixPtr(H) + 1; }
+
+ static Header* AllocateBuffer(std::string_view Str)
+ {
+ // new char[] is required to return memory aligned for any fundamental
+ // type whose size fits in the allocation, so placement-new of the
+ // atomic header at offset 0 is well-defined.
+ const size_t TotalBytes = sizeof(Header) + 1 + Str.size() + 1;
+ char* Bytes = new char[TotalBytes];
+ Header* H = new (Bytes) Header{};
+ H->RefCount.store(1, std::memory_order_relaxed);
+ char* Prefix = Bytes + sizeof(Header);
+ Prefix[0] = static_cast<char>(Str.size() < LengthFallbackSentinel ? Str.size() : LengthFallbackSentinel);
+ memcpy(Prefix + 1, Str.data(), Str.size());
+ Prefix[1 + Str.size()] = '\0';
+ return H;
+ }
+
+ static void ReleaseBuffer(Header* H) noexcept
+ {
+ if (!H)
+ {
+ return;
+ }
+ if (H->RefCount.fetch_sub(1, std::memory_order_acq_rel) == 1)
+ {
+ H->~Header();
+ delete[] reinterpret_cast<char*>(H);
+ }
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////
+
void string_forcelink(); // internal
} // namespace zen
diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp
index 4072aec56..8489841f8 100644
--- a/src/zencore/string.cpp
+++ b/src/zencore/string.cpp
@@ -1487,6 +1487,150 @@ TEST_CASE("CompactString.implicit_conversion")
CHECK(V == std::string_view("view"));
}
+TEST_CASE("SharedString.default")
+{
+ SharedString S;
+ CHECK(S.IsEmpty());
+ CHECK(S.Size() == 0);
+ CHECK(S.ToView() == std::string_view());
+ CHECK(S.c_str()[0] == '\0');
+ CHECK(S.UseCount() == 0);
+}
+
+TEST_CASE("SharedString.empty")
+{
+ // Empty input skips allocation: empty == default state.
+ SharedString S(std::string_view(""));
+ CHECK(S.IsEmpty());
+ CHECK(S.Size() == 0);
+ CHECK(S.UseCount() == 0);
+}
+
+TEST_CASE("SharedString.short")
+{
+ SharedString S(std::string_view("hello"));
+ CHECK(!S.IsEmpty());
+ CHECK(S.Size() == 5);
+ CHECK(S.ToView() == std::string_view("hello"));
+ CHECK(S.UseCount() == 1);
+}
+
+TEST_CASE("SharedString.sentinel_boundary")
+{
+ // 254 chars — largest value that fits in the prefix byte
+ std::string Str254(254, 'x');
+ std::string_view View254(Str254);
+ SharedString S(View254);
+ CHECK(S.Size() == 254);
+ CHECK(S.ToView() == View254);
+}
+
+TEST_CASE("SharedString.sentinel_exact")
+{
+ // 255 chars — hits the 0xFF sentinel, falls back to strlen
+ std::string Str255(255, 'y');
+ std::string_view View255(Str255);
+ SharedString S(View255);
+ CHECK(S.Size() == 255);
+ CHECK(S.ToView() == View255);
+}
+
+TEST_CASE("SharedString.long")
+{
+ std::string Str512(512, 'z');
+ std::string_view View512(Str512);
+ SharedString S(View512);
+ CHECK(S.Size() == 512);
+ CHECK(S.ToView() == View512);
+}
+
+TEST_CASE("SharedString.move")
+{
+ SharedString A(std::string_view("test"));
+ SharedString B(std::move(A));
+ CHECK(A.IsEmpty());
+ CHECK(B.ToView() == std::string_view("test"));
+ CHECK(B.UseCount() == 1);
+
+ SharedString C(std::string_view("first"));
+ SharedString D(std::string_view("second"));
+ D = std::move(C);
+ CHECK(C.IsEmpty());
+ CHECK(D.ToView() == std::string_view("first"));
+ CHECK(D.UseCount() == 1);
+}
+
+TEST_CASE("SharedString.copy_shares_buffer")
+{
+ SharedString A(std::string_view("shared"));
+ CHECK(A.UseCount() == 1);
+
+ SharedString B(A);
+ CHECK(A.UseCount() == 2);
+ CHECK(B.UseCount() == 2);
+ CHECK(A.ToView() == std::string_view("shared"));
+ CHECK(B.ToView() == std::string_view("shared"));
+ // Both views must point at the same underlying bytes.
+ CHECK(A.c_str() == B.c_str());
+
+ {
+ SharedString C = A;
+ CHECK(A.UseCount() == 3);
+ CHECK(C.c_str() == A.c_str());
+ }
+ // C has gone out of scope; refcount drops back.
+ CHECK(A.UseCount() == 2);
+}
+
+TEST_CASE("SharedString.copy_assign_releases_old")
+{
+ SharedString A(std::string_view("alpha"));
+ SharedString B(std::string_view("beta"));
+ CHECK(A.UseCount() == 1);
+ CHECK(B.UseCount() == 1);
+
+ B = A;
+ // B's old buffer is released; both now share A's buffer.
+ CHECK(A.UseCount() == 2);
+ CHECK(B.UseCount() == 2);
+ CHECK(B.ToView() == std::string_view("alpha"));
+ CHECK(A.c_str() == B.c_str());
+}
+
+TEST_CASE("SharedString.self_assign")
+{
+ SharedString A(std::string_view("self"));
+ CHECK(A.UseCount() == 1);
+
+ // Self copy-assignment must not release the buffer or change refcount.
+ SharedString& Aref = A;
+ A = Aref;
+ CHECK(A.UseCount() == 1);
+ CHECK(A.ToView() == std::string_view("self"));
+
+ // Self move-assignment must also be safe.
+ A = std::move(Aref);
+ CHECK(A.UseCount() == 1);
+ CHECK(A.ToView() == std::string_view("self"));
+}
+
+TEST_CASE("SharedString.empty_copy")
+{
+ SharedString A;
+ SharedString B(A);
+ CHECK(A.IsEmpty());
+ CHECK(B.IsEmpty());
+ CHECK(A.UseCount() == 0);
+ CHECK(B.UseCount() == 0);
+}
+
+TEST_CASE("SharedString.implicit_conversion")
+{
+ SharedString S(std::string_view("view"));
+ std::string_view V = S;
+ CHECK(V == std::string_view("view"));
+}
+
TEST_SUITE_END();
void