mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-20 21:43:41 +00:00
Add C++20 ownership and allocation core
Header-only primitives for the no-heap-after-startup design: Result/Status, Region, ObjectPool with PoolPtr (std::unique_ptr with a pool deleter), generation-checked Handle and asynchronous close support, intrusive RefPtr, FixedBuffer, MonotonicArena, FixedHashMap and FixedLru, IntrusiveList, and a bounded FixedHeap for third-party allocator hooks. Unit tests cover each primitive (test_core_*). scripts/check_no_heap.py rejects heap allocation in C++ sources under src/ and runs as a ctest. clang-tidy now also analyses C++ sources and the core headers.
This commit is contained in:
+5
-2
@@ -1,7 +1,10 @@
|
||||
# clang-tidy configuration for shadowsocks-c project sources (src/ only).
|
||||
# Bundled sources under third_party/ and src/blake3/ are third-party code and
|
||||
# are excluded by the file regex used in CI:
|
||||
# run-clang-tidy -p build '/src/[^/]+\.c$'
|
||||
# run-clang-tidy -p build '/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_core_[^/]+\.cpp)$'
|
||||
#
|
||||
# The header-only C++ core (src/core/*.hpp) is analysed through the
|
||||
# test_core_* translation units that include it.
|
||||
#
|
||||
# The check set is intentionally conservative: the full clang static analyzer
|
||||
# plus the bugprone/cert checks that are actionable for this codebase.
|
||||
@@ -33,4 +36,4 @@ Checks: >
|
||||
-clang-analyzer-security.insecureAPI.strcpy,
|
||||
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: '.*/src/[^/]+\.h$'
|
||||
HeaderFilterRegex: '.*/src/([^/]+\.h|core/[^/]+\.hpp)$'
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
run: |
|
||||
set -o pipefail
|
||||
tidy_status=0
|
||||
run-clang-tidy-18 -quiet -p build "$PWD/src/[^/]+\.c$" 2>&1 | tee tidy.log || tidy_status=$?
|
||||
run-clang-tidy-18 -quiet -p build "$PWD/(src/(core/|event/|relay/|crypto/|api/)?[^/]+\.(c|cpp)|tests/test_core_[^/]+\.cpp)$" 2>&1 | tee tidy.log || tidy_status=$?
|
||||
grep 'warning:' tidy.log | sort -u > warnings.txt || true
|
||||
count="$(wc -l < warnings.txt)"
|
||||
echo "clang-tidy warnings: $count (max allowed: $MAX_WARNINGS)"
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Reject heap allocation in the C++ sources under src/.
|
||||
|
||||
The C++ core follows a "no heap after startup" policy (docs/modernization.md):
|
||||
connections, buffers, crypto contexts and sessions come from fixed pools that
|
||||
are reserved once at startup. This lint keeps allocating constructs out of
|
||||
C++ code so the policy cannot erode one convenience at a time.
|
||||
|
||||
Only C++ files (.cpp, .hpp) are checked; the legacy C sources are ported
|
||||
module by module and keep their ss_malloc calls until then.
|
||||
|
||||
A line that must allocate (a startup reservation) carries a justification:
|
||||
|
||||
void *base = std::calloc(1, size); // heap-ok: startup reservation
|
||||
|
||||
Usage: check_no_heap.py [--root DIR] [--self-test]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
ALLOW_MARKER = "heap-ok:"
|
||||
|
||||
# (pattern, explanation). Patterns run on code with comments and string
|
||||
# literals blanked out.
|
||||
FORBIDDEN = [
|
||||
(re.compile(r"\b(?:std::)?(?:malloc|calloc|realloc|aligned_alloc|posix_memalign|strdup|strndup)\s*\("),
|
||||
"C allocator call"),
|
||||
(re.compile(r"\bss_(?:malloc|realloc|aligned_malloc|strndup)\s*\("), "ss_* allocator call"),
|
||||
(re.compile(r"\b(?:std::)?free\s*\("), "free(); memory comes from pools, not malloc"),
|
||||
# `new T`, `new T[n]`, `::new T` -- but not placement new `new (p) T`.
|
||||
(re.compile(r"(?<![\w.>])(?:::)?new\s+(?!\()[A-Za-z_:]"), "operator new"),
|
||||
(re.compile(r"\bdelete(?:\s*\[\s*\])?\s+[A-Za-z_*(]"), "operator delete"),
|
||||
(re.compile(r"\bstd::(?:make_unique|make_shared|allocate_shared|make_unique_for_overwrite)\b"),
|
||||
"heap-allocating smart pointer factory; acquire from an ObjectPool"),
|
||||
(re.compile(r"\bstd::(?:shared_ptr|weak_ptr)\b"), "std::shared_ptr; use ss::RefPtr over a pool"),
|
||||
(re.compile(r"\bstd::(?:vector|deque|list|forward_list|map|multimap|set|multiset|"
|
||||
r"unordered_map|unordered_multimap|unordered_set|unordered_multiset)\s*<"),
|
||||
"allocating container; use std::array, std::span or src/core containers"),
|
||||
(re.compile(r"\bstd::(?:string|wstring|u8string|stringstream|ostringstream|istringstream|function)\b"),
|
||||
"allocating type; use std::string_view, a FixedBuffer or a function pointer"),
|
||||
]
|
||||
|
||||
|
||||
def strip_comments_and_strings(text):
|
||||
"""Blank out comments and string/char literals, preserving line breaks."""
|
||||
out = []
|
||||
i, n = 0, len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if text.startswith("//", i):
|
||||
j = text.find("\n", i)
|
||||
j = n if j == -1 else j
|
||||
out.append(" " * (j - i))
|
||||
i = j
|
||||
elif text.startswith("/*", i):
|
||||
j = text.find("*/", i + 2)
|
||||
j = n if j == -1 else j + 2
|
||||
out.append("".join("\n" if ch == "\n" else " " for ch in text[i:j]))
|
||||
i = j
|
||||
elif c in "\"'":
|
||||
j = i + 1
|
||||
while j < n and text[j] != c and text[j] != "\n":
|
||||
j += 2 if text[j] == "\\" else 1
|
||||
j = min(j + 1, n)
|
||||
out.append(c + " " * (j - i - 2) + c if j - i >= 2 else c)
|
||||
i = j
|
||||
else:
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def check_text(text):
|
||||
"""Return a list of (line_number, explanation, source_line) violations."""
|
||||
problems = []
|
||||
raw_lines = text.splitlines()
|
||||
code_lines = strip_comments_and_strings(text).splitlines()
|
||||
for number, (raw, code) in enumerate(zip(raw_lines, code_lines), start=1):
|
||||
if ALLOW_MARKER in raw:
|
||||
continue
|
||||
for pattern, why in FORBIDDEN:
|
||||
if pattern.search(code):
|
||||
problems.append((number, why, raw.strip()))
|
||||
break
|
||||
return problems
|
||||
|
||||
|
||||
def check_tree(root):
|
||||
src = root / "src"
|
||||
files = sorted(p for p in src.rglob("*") if p.suffix in (".cpp", ".hpp") and "blake3" not in p.parts)
|
||||
failures = 0
|
||||
for path in files:
|
||||
for number, why, line in check_text(path.read_text(encoding="utf-8")):
|
||||
print(f"{path.relative_to(root)}:{number}: {why}: {line}")
|
||||
failures += 1
|
||||
print(f"check_no_heap: {len(files)} C++ files, {failures} violation(s)")
|
||||
return failures == 0
|
||||
|
||||
|
||||
def self_test():
|
||||
bad = {
|
||||
"int *p = (int *)malloc(4);": True,
|
||||
"auto *c = new Conn();": True,
|
||||
"auto *c = ::new Conn;": True,
|
||||
"delete c;": True,
|
||||
"std::vector<int> v;": True,
|
||||
"std::string name;": True,
|
||||
"std::function<void()> cb;": True,
|
||||
"auto p = std::make_unique<int>(1);": True,
|
||||
"std::shared_ptr<T> s;": True,
|
||||
"ss_free(ptr);": False, # not a call pattern we flag (macro wraps free)
|
||||
"free(buf);": True,
|
||||
}
|
||||
good = [
|
||||
"std::construct_at(p, 1);",
|
||||
"::new (static_cast<void *>(p)) T(1);",
|
||||
"Foo(const Foo &) = delete;",
|
||||
"std::string_view name;",
|
||||
"std::array<std::byte, 16> key;",
|
||||
"// std::vector<int> mentioned in a comment",
|
||||
'log("new connection from %s", addr);',
|
||||
"void *b = std::calloc(1, n); // heap-ok: startup reservation",
|
||||
"bool renew_lease = true;",
|
||||
"conn.free_slots();",
|
||||
]
|
||||
ok = True
|
||||
for line, expect in bad.items():
|
||||
got = bool(check_text(line))
|
||||
if got != expect:
|
||||
print(f"self-test: expected {'violation' if expect else 'clean'} for: {line}")
|
||||
ok = False
|
||||
for line in good:
|
||||
if check_text(line):
|
||||
print(f"self-test: false positive for: {line}")
|
||||
ok = False
|
||||
print("check_no_heap self-test:", "ok" if ok else "FAILED")
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path(__file__).resolve().parent.parent)
|
||||
parser.add_argument("--self-test", action="store_true", help="run the pattern self-test first")
|
||||
args = parser.parse_args()
|
||||
if args.self_test and not self_test():
|
||||
return 1
|
||||
return 0 if check_tree(args.root.resolve()) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,127 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_ARENA_HPP
|
||||
#define SS_CORE_ARENA_HPP
|
||||
|
||||
// MonotonicArena: bump allocation for data that lives as long as the
|
||||
// process configuration (parsed JSON, CLI strings, ACL entries).
|
||||
//
|
||||
// The arena reserves one block at startup. Allocations never free
|
||||
// individually; reset() discards everything at once (ss-manager uses this
|
||||
// per control command). Only trivially destructible types may be placed in
|
||||
// an arena, because nothing runs destructors.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "core/region.hpp"
|
||||
#include "core/result.hpp"
|
||||
|
||||
namespace ss {
|
||||
|
||||
class MonotonicArena {
|
||||
public:
|
||||
MonotonicArena() noexcept = default;
|
||||
MonotonicArena(const MonotonicArena &) = delete;
|
||||
MonotonicArena &operator=(const MonotonicArena &) = delete;
|
||||
MonotonicArena(MonotonicArena &&) = default;
|
||||
MonotonicArena &operator=(MonotonicArena &&) = default;
|
||||
|
||||
Status reserve(std::size_t bytes) noexcept
|
||||
{
|
||||
if (!block_.empty()) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
auto region = Region::reserve(bytes);
|
||||
if (!region) {
|
||||
return fail(region.error());
|
||||
}
|
||||
block_ = std::move(*region);
|
||||
used_ = 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
Result<std::span<std::byte> > allocate(std::size_t size, std::size_t align = alignof(std::max_align_t)) noexcept
|
||||
{
|
||||
if (align == 0 || (align & (align - 1)) != 0) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
auto base = reinterpret_cast<std::uintptr_t>(block_.data());
|
||||
auto current = base + used_;
|
||||
auto aligned = (current + align - 1) & ~(static_cast<std::uintptr_t>(align) - 1);
|
||||
std::size_t offset = static_cast<std::size_t>(aligned - base);
|
||||
if (block_.empty() || offset > block_.size() || size > block_.size() - offset) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
used_ = offset + size;
|
||||
if (used_ > peak_) {
|
||||
peak_ = used_;
|
||||
}
|
||||
return std::span<std::byte>(block_.data() + offset, size);
|
||||
}
|
||||
|
||||
template <class T, class... Args>
|
||||
Result<T *> make(Args &&...args) noexcept
|
||||
{
|
||||
static_assert(std::is_trivially_destructible_v<T>, "arena objects are never destroyed");
|
||||
auto bytes = allocate(sizeof(T), alignof(T));
|
||||
if (!bytes) {
|
||||
return fail(bytes.error());
|
||||
}
|
||||
return std::construct_at(reinterpret_cast<T *>(bytes->data()), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
Result<std::span<T> > make_array(std::size_t count) noexcept
|
||||
{
|
||||
static_assert(std::is_trivially_destructible_v<T> && std::is_default_constructible_v<T>);
|
||||
if (count > SIZE_MAX / sizeof(T)) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
auto bytes = allocate(sizeof(T) * count, alignof(T));
|
||||
if (!bytes) {
|
||||
return fail(bytes.error());
|
||||
}
|
||||
T *first = reinterpret_cast<T *>(bytes->data());
|
||||
for (std::size_t i = 0; i < count; ++i) {
|
||||
std::construct_at(first + i);
|
||||
}
|
||||
return std::span<T>(first, count);
|
||||
}
|
||||
|
||||
// Copy a string into the arena, NUL-terminated so it can be handed to C
|
||||
// APIs. The returned view excludes the terminator.
|
||||
Result<std::string_view> intern(std::string_view text) noexcept
|
||||
{
|
||||
auto bytes = allocate(text.size() + 1, 1);
|
||||
if (!bytes) {
|
||||
return fail(bytes.error());
|
||||
}
|
||||
char *out = reinterpret_cast<char *>(bytes->data());
|
||||
if (!text.empty()) {
|
||||
std::memcpy(out, text.data(), text.size());
|
||||
}
|
||||
out[text.size()] = '\0';
|
||||
return std::string_view(out, text.size());
|
||||
}
|
||||
|
||||
void reset() noexcept { used_ = 0; }
|
||||
|
||||
std::size_t capacity() const noexcept { return block_.size(); }
|
||||
std::size_t used() const noexcept { return used_; }
|
||||
std::size_t peak() const noexcept { return peak_; }
|
||||
|
||||
private:
|
||||
Region block_;
|
||||
std::size_t used_ = 0;
|
||||
std::size_t peak_ = 0;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,156 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_BUFFER_HPP
|
||||
#define SS_CORE_BUFFER_HPP
|
||||
|
||||
// FixedBuffer<N>: a byte buffer with inline, compile-time capacity.
|
||||
//
|
||||
// Replaces buffer_t (crypto.h) and balloc/brealloc/bprepend. The bytes live
|
||||
// inside the object, so a buffer embedded in a pooled connection costs no
|
||||
// allocation at all. Instead of growing, writers see how much room is left
|
||||
// through writable() and stop reading from the socket when it is full.
|
||||
//
|
||||
// Layout: [ consumed | readable (idx .. idx+len) | writable ]
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
|
||||
namespace ss {
|
||||
|
||||
using Bytes = std::span<std::byte>;
|
||||
using ConstBytes = std::span<const std::byte>;
|
||||
|
||||
inline ConstBytes as_bytes(const void *data, std::size_t size) noexcept
|
||||
{
|
||||
return { static_cast<const std::byte *>(data), size };
|
||||
}
|
||||
|
||||
template <std::size_t N>
|
||||
class FixedBuffer {
|
||||
static_assert(N > 0 && N <= UINT32_MAX);
|
||||
|
||||
public:
|
||||
static constexpr std::size_t kCapacity = N;
|
||||
|
||||
constexpr std::size_t capacity() const noexcept { return N; }
|
||||
constexpr std::size_t size() const noexcept { return len_; }
|
||||
constexpr bool empty() const noexcept { return len_ == 0; }
|
||||
constexpr bool full() const noexcept { return idx_ + len_ == N; }
|
||||
|
||||
ConstBytes readable() const noexcept { return { data_.data() + idx_, len_ }; }
|
||||
Bytes readable() noexcept { return { data_.data() + idx_, len_ }; }
|
||||
Bytes writable() noexcept { return { data_.data() + idx_ + len_, N - idx_ - len_ }; }
|
||||
|
||||
// Mark `n` bytes of writable() as filled.
|
||||
bool commit(std::size_t n) noexcept
|
||||
{
|
||||
if (n > N - idx_ - len_) {
|
||||
return false;
|
||||
}
|
||||
len_ += static_cast<std::uint32_t>(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drop `n` bytes from the front of readable().
|
||||
bool consume(std::size_t n) noexcept
|
||||
{
|
||||
if (n > len_) {
|
||||
return false;
|
||||
}
|
||||
idx_ += static_cast<std::uint32_t>(n);
|
||||
len_ -= static_cast<std::uint32_t>(n);
|
||||
if (len_ == 0) {
|
||||
idx_ = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Move the readable bytes to the front so writable() is as large as it
|
||||
// can be.
|
||||
void compact() noexcept
|
||||
{
|
||||
if (idx_ != 0) {
|
||||
if (len_ != 0) {
|
||||
std::memmove(data_.data(), data_.data() + idx_, len_);
|
||||
}
|
||||
idx_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool append(ConstBytes bytes) noexcept
|
||||
{
|
||||
if (bytes.size() > N - idx_ - len_) {
|
||||
compact();
|
||||
if (bytes.size() > N - len_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!bytes.empty()) {
|
||||
std::memcpy(data_.data() + idx_ + len_, bytes.data(), bytes.size());
|
||||
}
|
||||
len_ += static_cast<std::uint32_t>(bytes.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Insert bytes in front of readable(), e.g. a salt or an address header.
|
||||
bool prepend(ConstBytes bytes) noexcept
|
||||
{
|
||||
std::size_t n = bytes.size();
|
||||
if (n > N - len_) {
|
||||
return false;
|
||||
}
|
||||
if (n > idx_) {
|
||||
// Not enough headroom: shift the payload right.
|
||||
std::size_t new_idx = n;
|
||||
if (len_ != 0) {
|
||||
std::memmove(data_.data() + new_idx, data_.data() + idx_, len_);
|
||||
}
|
||||
idx_ = static_cast<std::uint32_t>(new_idx);
|
||||
}
|
||||
idx_ -= static_cast<std::uint32_t>(n);
|
||||
if (n != 0) {
|
||||
std::memcpy(data_.data() + idx_, bytes.data(), n);
|
||||
}
|
||||
len_ += static_cast<std::uint32_t>(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear() noexcept
|
||||
{
|
||||
idx_ = 0;
|
||||
len_ = 0;
|
||||
}
|
||||
|
||||
// Overwrite the contents. Fails, leaving the buffer unchanged, if the
|
||||
// bytes do not fit.
|
||||
bool assign(ConstBytes bytes) noexcept
|
||||
{
|
||||
if (bytes.size() > N) {
|
||||
return false;
|
||||
}
|
||||
clear();
|
||||
return append(bytes);
|
||||
}
|
||||
|
||||
// Wipe the storage, for buffers that held key material or plaintext.
|
||||
void wipe() noexcept
|
||||
{
|
||||
volatile std::byte *p = data_.data();
|
||||
for (std::size_t i = 0; i < N; ++i) {
|
||||
p[i] = std::byte{ 0 };
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
private:
|
||||
std::uint32_t idx_ = 0;
|
||||
std::uint32_t len_ = 0;
|
||||
std::array<std::byte, N> data_;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,229 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_FIXED_HEAP_HPP
|
||||
#define SS_CORE_FIXED_HEAP_HPP
|
||||
|
||||
// FixedHeap: a bounded malloc replacement for third-party libraries.
|
||||
//
|
||||
// libuv (uv_replace_allocator), c-ares (ares_library_init_mem) and Mbed TLS
|
||||
// (MBEDTLS_PLATFORM_MEMORY) can be pointed at our own allocation functions.
|
||||
// Each library gets its own FixedHeap, carved out of one region reserved at
|
||||
// startup, so their steady-state allocations stay inside a budget and can
|
||||
// never starve each other or the relay pools.
|
||||
//
|
||||
// Blocks come in power-of-two size classes from 32 bytes to 128 KiB, each
|
||||
// with a 16-byte header. Freed blocks go to a per-class free list and are
|
||||
// reused; untouched memory is handed out by bumping a pointer. Calls are
|
||||
// serialised with a spin lock, because libuv's thread pool may allocate
|
||||
// from worker threads.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include "core/region.hpp"
|
||||
#include "core/result.hpp"
|
||||
|
||||
namespace ss {
|
||||
|
||||
class FixedHeap {
|
||||
public:
|
||||
struct Stats {
|
||||
std::size_t in_use = 0; // bytes in live blocks, headers included
|
||||
std::size_t peak = 0;
|
||||
std::size_t reserved = 0;
|
||||
std::size_t allocations = 0;
|
||||
std::size_t failures = 0;
|
||||
};
|
||||
|
||||
FixedHeap() noexcept = default;
|
||||
FixedHeap(const FixedHeap &) = delete;
|
||||
FixedHeap &operator=(const FixedHeap &) = delete;
|
||||
|
||||
Status reserve(std::size_t budget) noexcept
|
||||
{
|
||||
if (!region_.empty()) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
auto region = Region::reserve(budget, kHeader);
|
||||
if (!region) {
|
||||
return fail(region.error());
|
||||
}
|
||||
region_ = std::move(*region);
|
||||
bump_ = 0;
|
||||
stats_.reserved = region_.size();
|
||||
return {};
|
||||
}
|
||||
|
||||
void *allocate(std::size_t size) noexcept
|
||||
{
|
||||
Guard guard(lock_);
|
||||
return allocate_locked(size);
|
||||
}
|
||||
|
||||
void *allocate_zeroed(std::size_t count, std::size_t size) noexcept
|
||||
{
|
||||
if (size != 0 && count > SIZE_MAX / size) {
|
||||
Guard guard(lock_);
|
||||
++stats_.failures;
|
||||
return nullptr;
|
||||
}
|
||||
std::size_t total = count * size;
|
||||
Guard guard(lock_);
|
||||
void *p = allocate_locked(total);
|
||||
if (p != nullptr) {
|
||||
std::memset(p, 0, total);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
void *reallocate(void *old, std::size_t size) noexcept
|
||||
{
|
||||
if (old == nullptr) {
|
||||
return allocate(size);
|
||||
}
|
||||
if (size == 0) {
|
||||
deallocate(old);
|
||||
return nullptr;
|
||||
}
|
||||
Guard guard(lock_);
|
||||
Header *h = header_of(old);
|
||||
std::size_t usable = class_size(h->cls) - kHeader;
|
||||
if (size <= usable) {
|
||||
return old;
|
||||
}
|
||||
void *fresh = allocate_locked(size);
|
||||
if (fresh != nullptr) {
|
||||
std::memcpy(fresh, old, usable);
|
||||
deallocate_locked(old);
|
||||
}
|
||||
return fresh;
|
||||
}
|
||||
|
||||
void deallocate(void *p) noexcept
|
||||
{
|
||||
if (p == nullptr) {
|
||||
return;
|
||||
}
|
||||
Guard guard(lock_);
|
||||
deallocate_locked(p);
|
||||
}
|
||||
|
||||
bool owns(const void *p) const noexcept
|
||||
{
|
||||
auto *b = static_cast<const std::byte *>(p);
|
||||
return b >= region_.data() && b < region_.data() + region_.size();
|
||||
}
|
||||
|
||||
Stats stats() const noexcept
|
||||
{
|
||||
Guard guard(lock_);
|
||||
return stats_;
|
||||
}
|
||||
|
||||
static constexpr std::size_t max_request() noexcept { return class_size(kClasses - 1) - kHeader; }
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kHeader = 16;
|
||||
static constexpr unsigned kMinShift = 5; // 32-byte blocks
|
||||
static constexpr unsigned kClasses = 13; // ... up to 128 KiB
|
||||
static constexpr std::uint32_t kMagic = 0x55aa1357U;
|
||||
static constexpr std::uint32_t kFreed = 0xdeadf7eeU;
|
||||
|
||||
struct Header {
|
||||
std::uint32_t magic;
|
||||
std::uint32_t cls;
|
||||
void *next_free; // valid only while the block is on a free list
|
||||
};
|
||||
static_assert(sizeof(Header) <= kHeader);
|
||||
|
||||
class Guard {
|
||||
public:
|
||||
explicit Guard(std::atomic_flag &flag) noexcept : flag_(flag)
|
||||
{
|
||||
while (flag_.test_and_set(std::memory_order_acquire)) {
|
||||
}
|
||||
}
|
||||
~Guard() { flag_.clear(std::memory_order_release); }
|
||||
Guard(const Guard &) = delete;
|
||||
Guard &operator=(const Guard &) = delete;
|
||||
|
||||
private:
|
||||
std::atomic_flag &flag_;
|
||||
};
|
||||
|
||||
static constexpr std::size_t class_size(unsigned cls) noexcept { return std::size_t{ 1 } << (cls + kMinShift); }
|
||||
|
||||
static Header *header_of(void *p) noexcept
|
||||
{
|
||||
auto *h = reinterpret_cast<Header *>(static_cast<std::byte *>(p) - kHeader);
|
||||
if (h->magic != kMagic || h->cls >= kClasses) {
|
||||
std::abort(); // not ours, or corrupted
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
void *allocate_locked(std::size_t size) noexcept
|
||||
{
|
||||
if (size == 0) {
|
||||
size = 1;
|
||||
}
|
||||
unsigned cls = 0;
|
||||
while (cls < kClasses && class_size(cls) - kHeader < size) {
|
||||
++cls;
|
||||
}
|
||||
if (cls == kClasses) {
|
||||
++stats_.failures;
|
||||
return nullptr;
|
||||
}
|
||||
std::byte *block = nullptr;
|
||||
if (free_[cls] != nullptr) {
|
||||
block = static_cast<std::byte *>(free_[cls]);
|
||||
SS_UNPOISON(block, class_size(cls));
|
||||
free_[cls] = reinterpret_cast<Header *>(block)->next_free;
|
||||
} else if (class_size(cls) <= region_.size() - bump_) {
|
||||
block = region_.data() + bump_;
|
||||
bump_ += class_size(cls);
|
||||
} else {
|
||||
++stats_.failures;
|
||||
return nullptr;
|
||||
}
|
||||
auto *h = reinterpret_cast<Header *>(block);
|
||||
h->magic = kMagic;
|
||||
h->cls = cls;
|
||||
h->next_free = nullptr;
|
||||
stats_.in_use += class_size(cls);
|
||||
++stats_.allocations;
|
||||
if (stats_.in_use > stats_.peak) {
|
||||
stats_.peak = stats_.in_use;
|
||||
}
|
||||
return block + kHeader;
|
||||
}
|
||||
|
||||
void deallocate_locked(void *p) noexcept
|
||||
{
|
||||
if (!owns(p)) {
|
||||
std::abort();
|
||||
}
|
||||
Header *h = header_of(p);
|
||||
unsigned cls = h->cls;
|
||||
stats_.in_use -= class_size(cls);
|
||||
h->magic = kFreed; // a second free of this block aborts in header_of
|
||||
h->next_free = free_[cls];
|
||||
free_[cls] = h;
|
||||
// Keep the header readable for the free list; poison the payload.
|
||||
SS_POISON(static_cast<std::byte *>(p), class_size(cls) - kHeader);
|
||||
}
|
||||
|
||||
Region region_;
|
||||
std::size_t bump_ = 0;
|
||||
void *free_[kClasses] = {};
|
||||
Stats stats_;
|
||||
mutable std::atomic_flag lock_ = ATOMIC_FLAG_INIT;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,495 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_FIXED_MAP_HPP
|
||||
#define SS_CORE_FIXED_MAP_HPP
|
||||
|
||||
// Fixed-capacity associative containers. Replace uthash (cache.c,
|
||||
// ss_event.c, manager.c) without any per-entry allocation.
|
||||
//
|
||||
// FixedHashMap<K, V>
|
||||
// Open addressing with linear probing and backward-shift deletion (no
|
||||
// tombstones, so lookups stay short however long the process runs).
|
||||
// The table is sized at reserve() time to at least twice the capacity.
|
||||
// Entries move when others are erased, so never keep a pointer to a
|
||||
// value across an erase.
|
||||
//
|
||||
// FixedLru<K, V>
|
||||
// Map plus recency order. Values live in a slab and never move, so the
|
||||
// pointer returned by insert() or find() stays valid until that entry
|
||||
// is erased or evicted. Replaces the pseudo-LRU in cache.c.
|
||||
//
|
||||
// Keys from the network (UDP peer addresses, session IDs) are hashed with a
|
||||
// per-table seed. Seed it from a CSPRNG in production code so a peer cannot
|
||||
// precompute colliding keys.
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
#include "core/buffer.hpp"
|
||||
#include "core/region.hpp"
|
||||
#include "core/result.hpp"
|
||||
|
||||
namespace ss {
|
||||
|
||||
constexpr std::uint64_t mix64(std::uint64_t x) noexcept
|
||||
{
|
||||
x ^= x >> 30;
|
||||
x *= 0xbf58476d1ce4e5b9ULL;
|
||||
x ^= x >> 27;
|
||||
x *= 0x94d049bb133111ebULL;
|
||||
x ^= x >> 31;
|
||||
return x;
|
||||
}
|
||||
|
||||
inline std::uint64_t hash_bytes(const void *data, std::size_t size, std::uint64_t seed) noexcept
|
||||
{
|
||||
const auto *p = static_cast<const unsigned char *>(data);
|
||||
std::uint64_t h = mix64(seed ^ (size * 0x9e3779b97f4a7c15ULL));
|
||||
while (size >= 8) {
|
||||
std::uint64_t chunk;
|
||||
std::memcpy(&chunk, p, 8);
|
||||
h = mix64(h ^ chunk) * 0x9e3779b97f4a7c15ULL;
|
||||
p += 8;
|
||||
size -= 8;
|
||||
}
|
||||
std::uint64_t tail = 0;
|
||||
if (size != 0) {
|
||||
std::memcpy(&tail, p, size);
|
||||
}
|
||||
return mix64(h ^ tail);
|
||||
}
|
||||
|
||||
// Inline byte-string key of at most N bytes (e.g. a sockaddr or an
|
||||
// AEAD-2022 session ID).
|
||||
template <std::size_t N>
|
||||
class FixedKey {
|
||||
static_assert(N > 0 && N <= 255);
|
||||
|
||||
public:
|
||||
constexpr FixedKey() noexcept = default;
|
||||
|
||||
static std::optional<FixedKey> from(ConstBytes bytes) noexcept
|
||||
{
|
||||
if (bytes.size() > N) {
|
||||
return std::nullopt;
|
||||
}
|
||||
FixedKey key;
|
||||
key.len_ = static_cast<std::uint8_t>(bytes.size());
|
||||
if (!bytes.empty()) {
|
||||
std::memcpy(key.data_.data(), bytes.data(), bytes.size());
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
ConstBytes bytes() const noexcept { return { data_.data(), len_ }; }
|
||||
std::size_t size() const noexcept { return len_; }
|
||||
|
||||
friend bool operator==(const FixedKey &a, const FixedKey &b) noexcept
|
||||
{
|
||||
return a.len_ == b.len_ && std::memcmp(a.data_.data(), b.data_.data(), a.len_) == 0;
|
||||
}
|
||||
|
||||
private:
|
||||
std::array<std::byte, N> data_{};
|
||||
std::uint8_t len_ = 0;
|
||||
};
|
||||
|
||||
template <class K>
|
||||
struct Hasher;
|
||||
|
||||
template <class K>
|
||||
requires std::is_integral_v<K> || std::is_enum_v<K>
|
||||
struct Hasher<K> {
|
||||
std::uint64_t operator()(K key, std::uint64_t seed) const noexcept
|
||||
{
|
||||
return mix64(static_cast<std::uint64_t>(key) ^ mix64(seed));
|
||||
}
|
||||
};
|
||||
|
||||
template <std::size_t N>
|
||||
struct Hasher<FixedKey<N> > {
|
||||
std::uint64_t operator()(const FixedKey<N> &key, std::uint64_t seed) const noexcept
|
||||
{
|
||||
return hash_bytes(key.bytes().data(), key.size(), seed);
|
||||
}
|
||||
};
|
||||
|
||||
template <class K, class V, class Hash = Hasher<K> >
|
||||
class FixedHashMap {
|
||||
static_assert(std::is_nothrow_move_constructible_v<K> && std::is_nothrow_move_constructible_v<V>);
|
||||
|
||||
public:
|
||||
FixedHashMap() noexcept = default;
|
||||
FixedHashMap(const FixedHashMap &) = delete;
|
||||
FixedHashMap &operator=(const FixedHashMap &) = delete;
|
||||
~FixedHashMap() { clear(); }
|
||||
|
||||
Status reserve(std::uint32_t capacity, std::uint64_t seed = 0) noexcept
|
||||
{
|
||||
if (capacity_ != 0 || capacity == 0 || capacity > (UINT32_MAX >> 2)) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
std::uint32_t slots = 2;
|
||||
while (slots < capacity * 2U) {
|
||||
slots <<= 1;
|
||||
}
|
||||
auto entries = Region::reserve(static_cast<std::size_t>(slots) * sizeof(Entry), alignof(Entry));
|
||||
if (!entries) {
|
||||
return fail(entries.error());
|
||||
}
|
||||
auto used = Region::reserve(slots, 1);
|
||||
if (!used) {
|
||||
return fail(used.error());
|
||||
}
|
||||
entries_ = std::move(*entries);
|
||||
used_ = std::move(*used);
|
||||
mask_ = slots - 1;
|
||||
capacity_ = capacity;
|
||||
seed_ = seed;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::uint32_t size() const noexcept { return size_; }
|
||||
std::uint32_t capacity() const noexcept { return capacity_; }
|
||||
bool empty() const noexcept { return size_ == 0; }
|
||||
bool full() const noexcept { return size_ == capacity_; }
|
||||
|
||||
V *find(const K &key) noexcept
|
||||
{
|
||||
std::uint32_t slot;
|
||||
return locate(key, slot) ? &entry(slot)->value : nullptr;
|
||||
}
|
||||
const V *find(const K &key) const noexcept { return const_cast<FixedHashMap *>(this)->find(key); }
|
||||
bool contains(const K &key) const noexcept { return find(key) != nullptr; }
|
||||
|
||||
// Insert a new key. Fails with Errc::exhausted when full and with
|
||||
// Errc::invalid when the key is already present.
|
||||
template <class... Args>
|
||||
Result<V *> emplace(const K &key, Args &&...args) noexcept
|
||||
{
|
||||
if (capacity_ == 0) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
std::uint32_t slot;
|
||||
if (locate(key, slot)) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
if (full()) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
// locate() leaves `slot` at the first empty slot of the probe run.
|
||||
Entry *e = std::construct_at(entry(slot), key, std::forward<Args>(args)...);
|
||||
used_.data()[slot] = std::byte{ 1 };
|
||||
++size_;
|
||||
return &e->value;
|
||||
}
|
||||
|
||||
std::optional<V> take(const K &key) noexcept
|
||||
{
|
||||
std::uint32_t slot;
|
||||
if (!locate(key, slot)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::optional<V> out(std::move(entry(slot)->value));
|
||||
remove_at(slot);
|
||||
return out;
|
||||
}
|
||||
|
||||
bool erase(const K &key) noexcept
|
||||
{
|
||||
std::uint32_t slot;
|
||||
if (!locate(key, slot)) {
|
||||
return false;
|
||||
}
|
||||
remove_at(slot);
|
||||
return true;
|
||||
}
|
||||
|
||||
void clear() noexcept
|
||||
{
|
||||
for (std::uint32_t i = 0; capacity_ != 0 && i <= mask_; ++i) {
|
||||
if (occupied(i)) {
|
||||
std::destroy_at(entry(i));
|
||||
used_.data()[i] = std::byte{ 0 };
|
||||
}
|
||||
}
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
// Visit every entry in unspecified order. The callback must not insert
|
||||
// or erase.
|
||||
template <class F>
|
||||
void for_each(F &&f)
|
||||
{
|
||||
for (std::uint32_t i = 0; capacity_ != 0 && i <= mask_; ++i) {
|
||||
if (occupied(i)) {
|
||||
f(static_cast<const K &>(entry(i)->key), entry(i)->value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
template <class... Args>
|
||||
explicit Entry(const K &k, Args &&...args) : key(k), value(std::forward<Args>(args)...)
|
||||
{
|
||||
}
|
||||
K key;
|
||||
V value;
|
||||
};
|
||||
|
||||
Entry *entry(std::uint32_t slot) const noexcept { return reinterpret_cast<Entry *>(entries_.data()) + slot; }
|
||||
bool occupied(std::uint32_t slot) const noexcept { return used_.data()[slot] != std::byte{ 0 }; }
|
||||
std::uint32_t home(const K &key) const noexcept
|
||||
{
|
||||
return static_cast<std::uint32_t>(Hash{}(key, seed_)) & mask_;
|
||||
}
|
||||
|
||||
// True if found (slot = its position); otherwise slot = first empty slot.
|
||||
bool locate(const K &key, std::uint32_t &slot) const noexcept
|
||||
{
|
||||
if (capacity_ == 0) {
|
||||
return false;
|
||||
}
|
||||
std::uint32_t i = home(key);
|
||||
while (occupied(i)) {
|
||||
if (entry(i)->key == key) {
|
||||
slot = i;
|
||||
return true;
|
||||
}
|
||||
i = (i + 1) & mask_;
|
||||
}
|
||||
slot = i;
|
||||
return false;
|
||||
}
|
||||
|
||||
void remove_at(std::uint32_t hole) noexcept
|
||||
{
|
||||
std::destroy_at(entry(hole));
|
||||
used_.data()[hole] = std::byte{ 0 };
|
||||
--size_;
|
||||
// Backward-shift: pull later members of the probe run into the hole
|
||||
// when the hole lies between their home slot and where they sit.
|
||||
std::uint32_t j = hole;
|
||||
for (;;) {
|
||||
j = (j + 1) & mask_;
|
||||
if (!occupied(j)) {
|
||||
return;
|
||||
}
|
||||
std::uint32_t k = home(entry(j)->key);
|
||||
bool movable = (hole <= j) ? (k <= hole || k > j) : (k <= hole && k > j);
|
||||
if (!movable) {
|
||||
continue;
|
||||
}
|
||||
std::construct_at(entry(hole), std::move(*entry(j)));
|
||||
used_.data()[hole] = std::byte{ 1 };
|
||||
std::destroy_at(entry(j));
|
||||
used_.data()[j] = std::byte{ 0 };
|
||||
hole = j;
|
||||
}
|
||||
}
|
||||
|
||||
Region entries_;
|
||||
Region used_;
|
||||
std::uint32_t mask_ = 0;
|
||||
std::uint32_t capacity_ = 0;
|
||||
std::uint32_t size_ = 0;
|
||||
std::uint64_t seed_ = 0;
|
||||
};
|
||||
|
||||
template <class K, class V, class Hash = Hasher<K> >
|
||||
class FixedLru {
|
||||
public:
|
||||
FixedLru() noexcept = default;
|
||||
FixedLru(const FixedLru &) = delete;
|
||||
FixedLru &operator=(const FixedLru &) = delete;
|
||||
~FixedLru() { clear(); }
|
||||
|
||||
Status reserve(std::uint32_t capacity, std::uint64_t seed = 0) noexcept
|
||||
{
|
||||
SS_TRY(index_.reserve(capacity, seed));
|
||||
auto nodes = Region::reserve(static_cast<std::size_t>(capacity) * sizeof(Node), alignof(Node));
|
||||
if (!nodes) {
|
||||
return fail(nodes.error());
|
||||
}
|
||||
nodes_ = std::move(*nodes);
|
||||
capacity_ = capacity;
|
||||
for (std::uint32_t i = 0; i < capacity; ++i) {
|
||||
std::construct_at(node(i))->next = (i + 1 < capacity) ? i + 1 : kNone;
|
||||
}
|
||||
free_ = 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
std::uint32_t size() const noexcept { return index_.size(); }
|
||||
std::uint32_t capacity() const noexcept { return capacity_; }
|
||||
bool empty() const noexcept { return size() == 0; }
|
||||
bool full() const noexcept { return free_ == kNone; }
|
||||
|
||||
// Look up and mark as most recently used.
|
||||
V *find(const K &key) noexcept
|
||||
{
|
||||
std::uint32_t *slot = index_.find(key);
|
||||
if (slot == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
touch(*slot);
|
||||
return value(*slot);
|
||||
}
|
||||
|
||||
// Look up without changing the recency order.
|
||||
V *peek(const K &key) noexcept
|
||||
{
|
||||
std::uint32_t *slot = index_.find(key);
|
||||
return slot == nullptr ? nullptr : value(*slot);
|
||||
}
|
||||
|
||||
// Insert as most recently used. Fails with Errc::exhausted when full
|
||||
// (evict with pop_oldest() first) and Errc::invalid for a duplicate key.
|
||||
template <class... Args>
|
||||
Result<V *> emplace(const K &key, Args &&...args) noexcept
|
||||
{
|
||||
if (free_ == kNone) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
std::uint32_t slot = free_;
|
||||
auto indexed = index_.emplace(key, slot);
|
||||
if (!indexed) {
|
||||
return fail(indexed.error());
|
||||
}
|
||||
Node *n = node(slot);
|
||||
free_ = n->next;
|
||||
std::construct_at(&n->key, key);
|
||||
std::construct_at(value(slot), std::forward<Args>(args)...);
|
||||
link_newest(slot);
|
||||
return value(slot);
|
||||
}
|
||||
|
||||
std::optional<V> take(const K &key) noexcept
|
||||
{
|
||||
auto slot = index_.take(key);
|
||||
if (!slot) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::optional<V>(release(*slot));
|
||||
}
|
||||
|
||||
bool erase(const K &key) noexcept { return take(key).has_value(); }
|
||||
|
||||
V *oldest() noexcept { return oldest_ == kNone ? nullptr : value(oldest_); }
|
||||
const K *oldest_key() const noexcept { return oldest_ == kNone ? nullptr : &node(oldest_)->key; }
|
||||
|
||||
// Remove the least recently used entry and hand its key and value to
|
||||
// the caller, who decides how to dispose of it.
|
||||
std::optional<std::pair<K, V> > pop_oldest() noexcept
|
||||
{
|
||||
if (oldest_ == kNone) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::uint32_t slot = oldest_;
|
||||
K key(node(slot)->key);
|
||||
index_.erase(key);
|
||||
V v = release(slot);
|
||||
return std::pair<K, V>(std::move(key), std::move(v));
|
||||
}
|
||||
|
||||
void clear() noexcept
|
||||
{
|
||||
while (oldest_ != kNone) {
|
||||
(void)pop_oldest();
|
||||
}
|
||||
}
|
||||
|
||||
// Visit entries from oldest to newest. The callback may not modify the
|
||||
// container.
|
||||
template <class F>
|
||||
void for_each(F &&f)
|
||||
{
|
||||
for (std::uint32_t s = oldest_; s != kNone; s = node(s)->next) {
|
||||
f(static_cast<const K &>(node(s)->key), *value(s));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::uint32_t kNone = UINT32_MAX;
|
||||
|
||||
struct Node {
|
||||
union {
|
||||
K key;
|
||||
};
|
||||
alignas(V) std::byte storage[sizeof(V)];
|
||||
std::uint32_t prev = kNone;
|
||||
std::uint32_t next = kNone;
|
||||
|
||||
Node() noexcept {}
|
||||
~Node() {}
|
||||
};
|
||||
|
||||
Node *node(std::uint32_t slot) const noexcept { return reinterpret_cast<Node *>(nodes_.data()) + slot; }
|
||||
V *value(std::uint32_t slot) const noexcept { return reinterpret_cast<V *>(node(slot)->storage); }
|
||||
|
||||
void link_newest(std::uint32_t slot) noexcept
|
||||
{
|
||||
Node *n = node(slot);
|
||||
n->prev = newest_;
|
||||
n->next = kNone;
|
||||
if (newest_ != kNone) {
|
||||
node(newest_)->next = slot;
|
||||
} else {
|
||||
oldest_ = slot;
|
||||
}
|
||||
newest_ = slot;
|
||||
}
|
||||
|
||||
void unlink(std::uint32_t slot) noexcept
|
||||
{
|
||||
Node *n = node(slot);
|
||||
if (n->prev != kNone) {
|
||||
node(n->prev)->next = n->next;
|
||||
} else {
|
||||
oldest_ = n->next;
|
||||
}
|
||||
if (n->next != kNone) {
|
||||
node(n->next)->prev = n->prev;
|
||||
} else {
|
||||
newest_ = n->prev;
|
||||
}
|
||||
n->prev = n->next = kNone;
|
||||
}
|
||||
|
||||
void touch(std::uint32_t slot) noexcept
|
||||
{
|
||||
if (slot != newest_) {
|
||||
unlink(slot);
|
||||
link_newest(slot);
|
||||
}
|
||||
}
|
||||
|
||||
V release(std::uint32_t slot) noexcept
|
||||
{
|
||||
unlink(slot);
|
||||
Node *n = node(slot);
|
||||
V out(std::move(*value(slot)));
|
||||
std::destroy_at(value(slot));
|
||||
std::destroy_at(&n->key);
|
||||
n->next = free_;
|
||||
free_ = slot;
|
||||
return out;
|
||||
}
|
||||
|
||||
FixedHashMap<K, std::uint32_t, Hash> index_;
|
||||
Region nodes_;
|
||||
std::uint32_t capacity_ = 0;
|
||||
std::uint32_t free_ = kNone;
|
||||
std::uint32_t oldest_ = kNone;
|
||||
std::uint32_t newest_ = kNone;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,137 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_INTRUSIVE_LIST_HPP
|
||||
#define SS_CORE_INTRUSIVE_LIST_HPP
|
||||
|
||||
// IntrusiveList<T, Tag>: a doubly linked list whose links live inside the
|
||||
// elements. Replaces ss_list (core.h).
|
||||
//
|
||||
// An element type derives from ListNode<Tag> once per list it can be on.
|
||||
// The list never owns its elements; it only links objects whose lifetime
|
||||
// is managed elsewhere (usually a pool). Destroying a linked node unlinks
|
||||
// it, so an element can never leave a dangling link behind.
|
||||
//
|
||||
// Owner recovery uses static_cast from the base class, which is well
|
||||
// defined, instead of ss_container_of pointer arithmetic.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace ss {
|
||||
|
||||
struct DefaultListTag;
|
||||
|
||||
template <class Tag = DefaultListTag>
|
||||
class ListNode {
|
||||
public:
|
||||
ListNode() noexcept = default;
|
||||
ListNode(const ListNode &) = delete;
|
||||
ListNode &operator=(const ListNode &) = delete;
|
||||
~ListNode() { unlink(); }
|
||||
|
||||
bool linked() const noexcept { return next_ != nullptr; }
|
||||
|
||||
void unlink() noexcept
|
||||
{
|
||||
if (next_ != nullptr) {
|
||||
prev_->next_ = next_;
|
||||
next_->prev_ = prev_;
|
||||
next_ = prev_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template <class, class>
|
||||
friend class IntrusiveList;
|
||||
|
||||
ListNode *prev_ = nullptr;
|
||||
ListNode *next_ = nullptr;
|
||||
};
|
||||
|
||||
template <class T, class Tag = DefaultListTag>
|
||||
class IntrusiveList {
|
||||
using Node = ListNode<Tag>;
|
||||
|
||||
public:
|
||||
IntrusiveList() noexcept { head_.prev_ = head_.next_ = &head_; }
|
||||
IntrusiveList(const IntrusiveList &) = delete;
|
||||
IntrusiveList &operator=(const IntrusiveList &) = delete;
|
||||
~IntrusiveList() { clear(); }
|
||||
|
||||
bool empty() const noexcept { return head_.next_ == &head_; }
|
||||
|
||||
std::size_t size() const noexcept
|
||||
{
|
||||
std::size_t n = 0;
|
||||
for (const Node *p = head_.next_; p != &head_; p = p->next_) {
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void push_back(T &item) noexcept { insert_before(&head_, node(item)); }
|
||||
void push_front(T &item) noexcept { insert_before(head_.next_, node(item)); }
|
||||
|
||||
// Move an element that is already on this list to the back.
|
||||
void move_to_back(T &item) noexcept
|
||||
{
|
||||
Node *n = node(item);
|
||||
n->unlink();
|
||||
insert_before(&head_, n);
|
||||
}
|
||||
|
||||
static void remove(T &item) noexcept { node(item)->unlink(); }
|
||||
|
||||
T *front() noexcept { return empty() ? nullptr : owner(head_.next_); }
|
||||
T *back() noexcept { return empty() ? nullptr : owner(head_.prev_); }
|
||||
|
||||
T *pop_front() noexcept
|
||||
{
|
||||
T *item = front();
|
||||
if (item != nullptr) {
|
||||
remove(*item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
// Unlink everything without touching the elements' lifetimes.
|
||||
void clear() noexcept
|
||||
{
|
||||
while (!empty()) {
|
||||
head_.next_->unlink();
|
||||
}
|
||||
}
|
||||
|
||||
// Visit every element. The callback may unlink or destroy the element
|
||||
// it was given (the next pointer is read first), but not others.
|
||||
template <class F>
|
||||
void for_each(F &&f)
|
||||
{
|
||||
Node *p = head_.next_;
|
||||
while (p != &head_) {
|
||||
Node *next = p->next_;
|
||||
f(*owner(p));
|
||||
p = next;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static Node *node(T &item) noexcept { return static_cast<Node *>(&item); }
|
||||
static T *owner(Node *n) noexcept { return static_cast<T *>(n); }
|
||||
|
||||
static void insert_before(Node *pos, Node *n) noexcept
|
||||
{
|
||||
if (n->linked()) {
|
||||
std::abort(); // already on a list using this tag
|
||||
}
|
||||
n->next_ = pos;
|
||||
n->prev_ = pos->prev_;
|
||||
pos->prev_->next_ = n;
|
||||
pos->prev_ = n;
|
||||
}
|
||||
|
||||
Node head_;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,282 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_POOL_HPP
|
||||
#define SS_CORE_POOL_HPP
|
||||
|
||||
// ObjectPool<T>: fixed-capacity storage for objects of one type.
|
||||
//
|
||||
// Ownership model
|
||||
// * acquire() constructs a T in a free slot and returns PoolPtr<T>, a
|
||||
// std::unique_ptr whose deleter hands the slot back to the pool. The
|
||||
// PoolPtr is the one and only owner.
|
||||
// * Anything that needs to refer to the object without owning it (a DNS
|
||||
// callback, a timer, the peer of a relay) keeps a Handle<T>. A handle
|
||||
// carries the slot's generation, so resolve() returns nullptr once the
|
||||
// object is gone, even if the slot has been reused since.
|
||||
// * A type that embeds libuv handles cannot be freed while libuv still
|
||||
// points at them. Such types model AsyncClosable: when the owner drops
|
||||
// the PoolPtr, the slot enters the closing state (handles stop
|
||||
// resolving), T::begin_close() starts closing the libuv handles, and the
|
||||
// slot is destroyed and recycled only when T invokes the completion.
|
||||
//
|
||||
// The pool reserves all of its memory up front, so acquire() and release
|
||||
// never call the system allocator. Free slots are poisoned under ASan, which
|
||||
// keeps use-after-free detectable even though memory is recycled.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <utility>
|
||||
|
||||
#include "core/region.hpp"
|
||||
#include "core/result.hpp"
|
||||
|
||||
namespace ss {
|
||||
|
||||
template <class T>
|
||||
class ObjectPool;
|
||||
|
||||
template <class T>
|
||||
struct Handle {
|
||||
std::uint32_t index = UINT32_MAX;
|
||||
std::uint32_t gen = 0;
|
||||
|
||||
constexpr explicit operator bool() const noexcept { return gen != 0; }
|
||||
friend constexpr bool operator==(const Handle &, const Handle &) = default;
|
||||
};
|
||||
|
||||
// Passed to T::begin_close(). T must invoke it exactly once, after its last
|
||||
// asynchronous close has finished; it may also be invoked synchronously from
|
||||
// inside begin_close(). The object is destroyed during the call, so T must
|
||||
// not touch itself afterwards.
|
||||
class CloseCompletion {
|
||||
public:
|
||||
CloseCompletion() noexcept = default;
|
||||
CloseCompletion(void (*fn)(void *, std::uint32_t), void *pool, std::uint32_t index) noexcept
|
||||
: fn_(fn), pool_(pool), index_(index)
|
||||
{
|
||||
}
|
||||
|
||||
void operator()() noexcept
|
||||
{
|
||||
auto fn = std::exchange(fn_, nullptr);
|
||||
if (fn == nullptr) {
|
||||
std::abort(); // completed twice, or never armed
|
||||
}
|
||||
fn(pool_, index_);
|
||||
}
|
||||
|
||||
explicit operator bool() const noexcept { return fn_ != nullptr; }
|
||||
|
||||
private:
|
||||
void (*fn_)(void *, std::uint32_t) = nullptr;
|
||||
void *pool_ = nullptr;
|
||||
std::uint32_t index_ = 0;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
concept AsyncClosable = requires(T &t, CloseCompletion done) {
|
||||
t.begin_close(done);
|
||||
};
|
||||
|
||||
template <class T>
|
||||
struct PoolDeleter {
|
||||
ObjectPool<T> *pool = nullptr;
|
||||
|
||||
void operator()(T *object) const noexcept
|
||||
{
|
||||
if (pool == nullptr) {
|
||||
std::abort(); // a PoolPtr must come from ObjectPool::acquire
|
||||
}
|
||||
pool->release(object);
|
||||
}
|
||||
};
|
||||
|
||||
template <class T>
|
||||
using PoolPtr = std::unique_ptr<T, PoolDeleter<T> >;
|
||||
|
||||
template <class T>
|
||||
class ObjectPool {
|
||||
static_assert(std::is_nothrow_destructible_v<T>);
|
||||
|
||||
public:
|
||||
ObjectPool() noexcept = default;
|
||||
ObjectPool(const ObjectPool &) = delete;
|
||||
ObjectPool &operator=(const ObjectPool &) = delete;
|
||||
ObjectPool(ObjectPool &&) = delete;
|
||||
ObjectPool &operator=(ObjectPool &&) = delete;
|
||||
|
||||
~ObjectPool()
|
||||
{
|
||||
// Every PoolPtr must be gone and every close finished before the
|
||||
// pool goes away; otherwise a deleter or close callback would write
|
||||
// into freed memory.
|
||||
if (live_ != 0 || closing_ != 0) {
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve storage for `capacity` objects. Called once at startup.
|
||||
Status reserve(std::uint32_t capacity) noexcept
|
||||
{
|
||||
if (capacity_ != 0 || capacity == 0 || capacity == UINT32_MAX) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
auto objects = Region::reserve(static_cast<std::size_t>(capacity) * sizeof(T), alignof(T));
|
||||
if (!objects) {
|
||||
return fail(objects.error());
|
||||
}
|
||||
auto meta = Region::reserve(static_cast<std::size_t>(capacity) * sizeof(Meta), alignof(Meta));
|
||||
if (!meta) {
|
||||
return fail(meta.error());
|
||||
}
|
||||
objects_ = std::move(*objects);
|
||||
meta_ = std::move(*meta);
|
||||
capacity_ = capacity;
|
||||
for (std::uint32_t i = 0; i < capacity; ++i) {
|
||||
Meta *m = std::construct_at(meta_at(i));
|
||||
m->next = (i + 1 < capacity) ? i + 1 : kNone;
|
||||
}
|
||||
free_head_ = 0;
|
||||
SS_POISON(objects_.data(), objects_.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
template <class... Args>
|
||||
Result<PoolPtr<T> > acquire(Args &&...args) noexcept
|
||||
{
|
||||
if (free_head_ == kNone) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
std::uint32_t index = free_head_;
|
||||
Meta *m = meta_at(index);
|
||||
free_head_ = m->next;
|
||||
m->next = kNone;
|
||||
m->state = State::live;
|
||||
++live_;
|
||||
void *raw = slot_at(index);
|
||||
SS_UNPOISON(raw, sizeof(T));
|
||||
T *object = std::construct_at(static_cast<T *>(raw), std::forward<Args>(args)...);
|
||||
return PoolPtr<T>(object, PoolDeleter<T>{ this });
|
||||
}
|
||||
|
||||
// Handle for a live object owned by this pool.
|
||||
Handle<T> handle_of(const T *object) const noexcept
|
||||
{
|
||||
std::uint32_t index = index_of(object);
|
||||
return Handle<T>{ index, meta_at(index)->gen };
|
||||
}
|
||||
|
||||
// The object a handle refers to, or nullptr once it is closing or gone.
|
||||
T *resolve(Handle<T> handle) const noexcept
|
||||
{
|
||||
if (handle.index >= capacity_) {
|
||||
return nullptr;
|
||||
}
|
||||
const Meta *m = meta_at(handle.index);
|
||||
if (m->state != State::live || m->gen != handle.gen) {
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<T *>(slot_at(handle.index));
|
||||
}
|
||||
|
||||
bool owns(const T *object) const noexcept
|
||||
{
|
||||
auto *p = reinterpret_cast<const std::byte *>(object);
|
||||
auto *first = objects_.data();
|
||||
return first != nullptr && p >= first && p < first + objects_.size();
|
||||
}
|
||||
|
||||
std::uint32_t capacity() const noexcept { return capacity_; }
|
||||
std::uint32_t live() const noexcept { return live_; }
|
||||
std::uint32_t closing() const noexcept { return closing_; }
|
||||
std::uint32_t available() const noexcept { return capacity_ - live_ - closing_; }
|
||||
bool full() const noexcept { return free_head_ == kNone; }
|
||||
|
||||
private:
|
||||
friend struct PoolDeleter<T>;
|
||||
|
||||
enum class State : std::uint8_t { free, live, closing };
|
||||
|
||||
struct Meta {
|
||||
std::uint32_t gen = 1; // 0 is reserved for the empty handle
|
||||
std::uint32_t next = kNone;
|
||||
State state = State::free;
|
||||
};
|
||||
|
||||
static constexpr std::uint32_t kNone = UINT32_MAX;
|
||||
|
||||
void *slot_at(std::uint32_t index) const noexcept
|
||||
{
|
||||
return objects_.data() + static_cast<std::size_t>(index) * sizeof(T);
|
||||
}
|
||||
Meta *meta_at(std::uint32_t index) const noexcept
|
||||
{
|
||||
return reinterpret_cast<Meta *>(meta_.data()) + index;
|
||||
}
|
||||
|
||||
std::uint32_t index_of(const T *object) const noexcept
|
||||
{
|
||||
if (!owns(object)) {
|
||||
std::abort();
|
||||
}
|
||||
auto offset = static_cast<std::size_t>(reinterpret_cast<const std::byte *>(object) - objects_.data());
|
||||
if (offset % sizeof(T) != 0) {
|
||||
std::abort();
|
||||
}
|
||||
return static_cast<std::uint32_t>(offset / sizeof(T));
|
||||
}
|
||||
|
||||
void release(T *object) noexcept
|
||||
{
|
||||
std::uint32_t index = index_of(object);
|
||||
Meta *m = meta_at(index);
|
||||
if (m->state != State::live) {
|
||||
std::abort(); // double release
|
||||
}
|
||||
--live_;
|
||||
if constexpr (AsyncClosable<T>) {
|
||||
m->state = State::closing;
|
||||
++closing_;
|
||||
object->begin_close(CloseCompletion(&ObjectPool::closed_trampoline, this, index));
|
||||
} else {
|
||||
recycle(index);
|
||||
}
|
||||
}
|
||||
|
||||
static void closed_trampoline(void *pool, std::uint32_t index) noexcept
|
||||
{
|
||||
auto *self = static_cast<ObjectPool *>(pool);
|
||||
Meta *m = self->meta_at(index);
|
||||
if (m->state != State::closing) {
|
||||
std::abort();
|
||||
}
|
||||
--self->closing_;
|
||||
self->recycle(index);
|
||||
}
|
||||
|
||||
void recycle(std::uint32_t index) noexcept
|
||||
{
|
||||
Meta *m = meta_at(index);
|
||||
std::destroy_at(static_cast<T *>(slot_at(index)));
|
||||
SS_POISON(slot_at(index), sizeof(T));
|
||||
m->state = State::free;
|
||||
if (++m->gen == 0) {
|
||||
m->gen = 1;
|
||||
}
|
||||
m->next = free_head_;
|
||||
free_head_ = index;
|
||||
}
|
||||
|
||||
Region objects_;
|
||||
Region meta_;
|
||||
std::uint32_t capacity_ = 0;
|
||||
std::uint32_t free_head_ = kNone;
|
||||
std::uint32_t live_ = 0;
|
||||
std::uint32_t closing_ = 0;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,135 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_REF_HPP
|
||||
#define SS_CORE_REF_HPP
|
||||
|
||||
// RefCounted<T> / RefPtr<T>: intrusive shared ownership.
|
||||
//
|
||||
// Use this only where ownership is genuinely shared. Today that means
|
||||
// AEAD-2022 UDP sessions, which are held by the session table and by every
|
||||
// relay slot that is still talking to the peer. Everything else has a single
|
||||
// owner and uses std::unique_ptr (usually PoolPtr).
|
||||
//
|
||||
// Why not std::shared_ptr: std::allocate_shared has an implementation-
|
||||
// defined control block that cannot be sized into a pool portably, weak_ptr
|
||||
// would keep the slot alive after the object dies, and the atomic count is
|
||||
// wasted on a single-threaded event loop. The count here is a plain integer;
|
||||
// RefPtr must not cross threads.
|
||||
//
|
||||
// T derives from RefCounted<T> and provides
|
||||
// static void destroy(T *object) noexcept;
|
||||
// which runs when the last reference goes away (typically by dropping the
|
||||
// PoolPtr that owns the storage).
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
|
||||
namespace ss {
|
||||
|
||||
template <class T>
|
||||
class RefPtr;
|
||||
|
||||
template <class T>
|
||||
class RefCounted {
|
||||
public:
|
||||
RefCounted(const RefCounted &) = delete;
|
||||
RefCounted &operator=(const RefCounted &) = delete;
|
||||
|
||||
std::size_t ref_count() const noexcept { return count_; }
|
||||
|
||||
protected:
|
||||
~RefCounted()
|
||||
{
|
||||
if (count_ != 0) {
|
||||
std::abort(); // destroyed while references remain
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
friend T;
|
||||
friend class RefPtr<T>;
|
||||
|
||||
RefCounted() noexcept = default;
|
||||
|
||||
void retain() noexcept
|
||||
{
|
||||
if (count_ == SIZE_MAX) {
|
||||
std::abort();
|
||||
}
|
||||
++count_;
|
||||
}
|
||||
|
||||
void release() noexcept
|
||||
{
|
||||
if (count_ == 0) {
|
||||
std::abort();
|
||||
}
|
||||
if (--count_ == 0) {
|
||||
T::destroy(static_cast<T *>(this));
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t count_ = 0;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
class RefPtr {
|
||||
public:
|
||||
constexpr RefPtr() noexcept = default;
|
||||
constexpr RefPtr(std::nullptr_t) noexcept {} // NOLINT(google-explicit-constructor)
|
||||
|
||||
// Takes the first reference, or adds one to an object that is already
|
||||
// shared.
|
||||
explicit RefPtr(T *object) noexcept : ptr_(object)
|
||||
{
|
||||
if (ptr_ != nullptr) {
|
||||
ptr_->retain();
|
||||
}
|
||||
}
|
||||
|
||||
RefPtr(const RefPtr &other) noexcept : RefPtr(other.ptr_) {}
|
||||
RefPtr(RefPtr &&other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) {}
|
||||
|
||||
RefPtr &operator=(const RefPtr &other) noexcept
|
||||
{
|
||||
RefPtr(other).swap(*this);
|
||||
return *this;
|
||||
}
|
||||
RefPtr &operator=(RefPtr &&other) noexcept
|
||||
{
|
||||
RefPtr(std::move(other)).swap(*this);
|
||||
return *this;
|
||||
}
|
||||
RefPtr &operator=(std::nullptr_t) noexcept
|
||||
{
|
||||
reset();
|
||||
return *this;
|
||||
}
|
||||
|
||||
~RefPtr() { reset(); }
|
||||
|
||||
void reset() noexcept
|
||||
{
|
||||
if (T *p = std::exchange(ptr_, nullptr)) {
|
||||
p->release();
|
||||
}
|
||||
}
|
||||
|
||||
void swap(RefPtr &other) noexcept { std::swap(ptr_, other.ptr_); }
|
||||
|
||||
T *get() const noexcept { return ptr_; }
|
||||
T &operator*() const noexcept { return *ptr_; }
|
||||
T *operator->() const noexcept { return ptr_; }
|
||||
explicit operator bool() const noexcept { return ptr_ != nullptr; }
|
||||
|
||||
friend bool operator==(const RefPtr &a, const RefPtr &b) noexcept { return a.ptr_ == b.ptr_; }
|
||||
friend bool operator==(const RefPtr &a, std::nullptr_t) noexcept { return a.ptr_ == nullptr; }
|
||||
|
||||
private:
|
||||
T *ptr_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,117 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_REGION_HPP
|
||||
#define SS_CORE_REGION_HPP
|
||||
|
||||
// Region: one aligned block of raw memory obtained from the system allocator.
|
||||
//
|
||||
// This is the only place in src/core that calls malloc. Pools, arenas and
|
||||
// tables reserve their storage through a Region while the process starts up;
|
||||
// after that the steady-state data path never touches the system allocator.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include "core/result.hpp"
|
||||
|
||||
#if defined(__has_feature)
|
||||
#if __has_feature(address_sanitizer)
|
||||
#define SS_HAVE_ASAN 1
|
||||
#endif
|
||||
#endif
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
#define SS_HAVE_ASAN 1
|
||||
#endif
|
||||
|
||||
#ifdef SS_HAVE_ASAN
|
||||
#include <sanitizer/asan_interface.h>
|
||||
#define SS_POISON(addr, size) ASAN_POISON_MEMORY_REGION((addr), (size))
|
||||
#define SS_UNPOISON(addr, size) ASAN_UNPOISON_MEMORY_REGION((addr), (size))
|
||||
#else
|
||||
#define SS_POISON(addr, size) ((void)(addr), (void)(size))
|
||||
#define SS_UNPOISON(addr, size) ((void)(addr), (void)(size))
|
||||
#endif
|
||||
|
||||
namespace ss {
|
||||
|
||||
class Region {
|
||||
public:
|
||||
Region() noexcept = default;
|
||||
Region(const Region &) = delete;
|
||||
Region &operator=(const Region &) = delete;
|
||||
Region(Region &&other) noexcept
|
||||
: base_(std::exchange(other.base_, nullptr))
|
||||
, data_(std::exchange(other.data_, nullptr))
|
||||
, size_(std::exchange(other.size_, 0))
|
||||
{
|
||||
}
|
||||
Region &operator=(Region &&other) noexcept
|
||||
{
|
||||
if (this != &other) {
|
||||
release();
|
||||
base_ = std::exchange(other.base_, nullptr);
|
||||
data_ = std::exchange(other.data_, nullptr);
|
||||
size_ = std::exchange(other.size_, 0);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
~Region() { release(); }
|
||||
|
||||
// Reserve `size` zeroed bytes aligned to `align` (a power of two).
|
||||
static Result<Region> reserve(std::size_t size, std::size_t align = alignof(std::max_align_t)) noexcept
|
||||
{
|
||||
if (align == 0 || (align & (align - 1)) != 0) {
|
||||
return fail(Errc::invalid);
|
||||
}
|
||||
if (align < alignof(std::max_align_t)) {
|
||||
align = alignof(std::max_align_t);
|
||||
}
|
||||
if (size == 0) {
|
||||
return Region{};
|
||||
}
|
||||
if (size > SIZE_MAX - align) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
// Over-allocate and align by hand: aligned_alloc is missing on
|
||||
// MinGW, and _aligned_malloc would need its own free function.
|
||||
void *base = std::calloc(1, size + align); // heap-ok: startup reservation
|
||||
if (base == nullptr) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
auto addr = reinterpret_cast<std::uintptr_t>(base);
|
||||
auto aligned = (addr + align - 1) & ~(static_cast<std::uintptr_t>(align) - 1);
|
||||
Region r;
|
||||
r.base_ = base;
|
||||
r.data_ = reinterpret_cast<std::byte *>(aligned);
|
||||
r.size_ = size;
|
||||
return r;
|
||||
}
|
||||
|
||||
std::byte *data() const noexcept { return data_; }
|
||||
std::size_t size() const noexcept { return size_; }
|
||||
bool empty() const noexcept { return size_ == 0; }
|
||||
|
||||
private:
|
||||
void release() noexcept
|
||||
{
|
||||
if (base_ != nullptr) {
|
||||
// Pools may leave freed slots poisoned; the allocator needs to
|
||||
// be able to reuse the whole block.
|
||||
SS_UNPOISON(data_, size_);
|
||||
std::free(base_); // heap-ok: startup reservation
|
||||
}
|
||||
base_ = nullptr;
|
||||
data_ = nullptr;
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
void *base_ = nullptr;
|
||||
std::byte *data_ = nullptr;
|
||||
std::size_t size_ = 0;
|
||||
};
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,236 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_CORE_RESULT_HPP
|
||||
#define SS_CORE_RESULT_HPP
|
||||
|
||||
// Error-or-value return type for code built without exceptions.
|
||||
//
|
||||
// The interface is a subset of C++23 std::expected (has_value, value, error,
|
||||
// and_then, transform, value_or) so Result can become an alias of
|
||||
// std::expected once the toolchain floor allows it.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
|
||||
namespace ss {
|
||||
|
||||
enum class Errc : std::uint8_t {
|
||||
exhausted, // a fixed-capacity pool or table is full
|
||||
io, // socket or file error; Error::sys holds errno
|
||||
again, // operation would block
|
||||
crypto, // authentication or cipher failure
|
||||
protocol, // malformed peer input
|
||||
config, // invalid configuration
|
||||
closed, // object is closing or already closed
|
||||
invalid, // invalid argument from the caller
|
||||
};
|
||||
|
||||
struct Error {
|
||||
Errc code;
|
||||
int sys = 0;
|
||||
|
||||
friend constexpr bool operator==(const Error &, const Error &) = default;
|
||||
};
|
||||
|
||||
template <class E>
|
||||
struct Unexpected {
|
||||
E value;
|
||||
};
|
||||
|
||||
template <class E>
|
||||
constexpr Unexpected<std::decay_t<E> > fail(E &&e)
|
||||
{
|
||||
return { std::forward<E>(e) };
|
||||
}
|
||||
|
||||
constexpr Unexpected<Error> fail(Errc code, int sys = 0)
|
||||
{
|
||||
return { Error{ code, sys } };
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
[[noreturn]] inline void result_misuse() noexcept
|
||||
{
|
||||
// Accessing the wrong alternative is a programming error. There is no
|
||||
// exception to throw, so stop here instead of reading garbage.
|
||||
std::abort();
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
template <class T, class E = Error>
|
||||
class [[nodiscard]] Result {
|
||||
static_assert(!std::is_reference_v<T>, "Result<T&> is not supported; use a pointer");
|
||||
|
||||
public:
|
||||
using value_type = T;
|
||||
using error_type = E;
|
||||
|
||||
template <class U = T>
|
||||
requires std::is_constructible_v<T, U &&>
|
||||
constexpr Result(U &&v) noexcept(std::is_nothrow_constructible_v<T, U &&>) // NOLINT(google-explicit-constructor)
|
||||
: ok_(true)
|
||||
{
|
||||
std::construct_at(&value_, std::forward<U>(v));
|
||||
}
|
||||
|
||||
template <class G>
|
||||
constexpr Result(Unexpected<G> &&e) noexcept // NOLINT(google-explicit-constructor)
|
||||
: ok_(false)
|
||||
{
|
||||
std::construct_at(&error_, std::move(e.value));
|
||||
}
|
||||
|
||||
Result(const Result &other) requires std::is_copy_constructible_v<T>
|
||||
: ok_(other.ok_)
|
||||
{
|
||||
if (ok_) {
|
||||
std::construct_at(&value_, other.value_);
|
||||
} else {
|
||||
std::construct_at(&error_, other.error_);
|
||||
}
|
||||
}
|
||||
|
||||
Result(Result &&other) noexcept(std::is_nothrow_move_constructible_v<T>)
|
||||
: ok_(other.ok_)
|
||||
{
|
||||
if (ok_) {
|
||||
std::construct_at(&value_, std::move(other.value_));
|
||||
} else {
|
||||
std::construct_at(&error_, std::move(other.error_));
|
||||
}
|
||||
}
|
||||
|
||||
Result &operator=(const Result &) = delete;
|
||||
Result &operator=(Result &&) = delete;
|
||||
|
||||
~Result()
|
||||
{
|
||||
if (ok_) {
|
||||
std::destroy_at(&value_);
|
||||
} else {
|
||||
std::destroy_at(&error_);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr bool has_value() const noexcept { return ok_; }
|
||||
constexpr explicit operator bool() const noexcept { return ok_; }
|
||||
|
||||
constexpr T &value() & noexcept
|
||||
{
|
||||
if (!ok_) {
|
||||
detail::result_misuse();
|
||||
}
|
||||
return value_;
|
||||
}
|
||||
constexpr const T &value() const & noexcept
|
||||
{
|
||||
if (!ok_) {
|
||||
detail::result_misuse();
|
||||
}
|
||||
return value_;
|
||||
}
|
||||
constexpr T &&value() && noexcept
|
||||
{
|
||||
if (!ok_) {
|
||||
detail::result_misuse();
|
||||
}
|
||||
return std::move(value_);
|
||||
}
|
||||
|
||||
constexpr T &operator*() & noexcept { return value(); }
|
||||
constexpr const T &operator*() const & noexcept { return value(); }
|
||||
constexpr T &&operator*() && noexcept { return std::move(*this).value(); }
|
||||
constexpr T *operator->() noexcept { return &value(); }
|
||||
constexpr const T *operator->() const noexcept { return &value(); }
|
||||
|
||||
constexpr const E &error() const noexcept
|
||||
{
|
||||
if (ok_) {
|
||||
detail::result_misuse();
|
||||
}
|
||||
return error_;
|
||||
}
|
||||
|
||||
template <class U>
|
||||
constexpr T value_or(U &&fallback) const & { return ok_ ? value_ : static_cast<T>(std::forward<U>(fallback)); }
|
||||
|
||||
template <class F>
|
||||
constexpr auto and_then(F &&f) &&
|
||||
{
|
||||
using R = std::invoke_result_t<F, T &&>;
|
||||
if (ok_) {
|
||||
return std::forward<F>(f)(std::move(value_));
|
||||
}
|
||||
return R(Unexpected<E>{ error_ });
|
||||
}
|
||||
|
||||
template <class F>
|
||||
constexpr auto transform(F &&f) &&
|
||||
{
|
||||
using U = std::invoke_result_t<F, T &&>;
|
||||
if (ok_) {
|
||||
return Result<U, E>(std::forward<F>(f)(std::move(value_)));
|
||||
}
|
||||
return Result<U, E>(Unexpected<E>{ error_ });
|
||||
}
|
||||
|
||||
private:
|
||||
union {
|
||||
T value_;
|
||||
E error_;
|
||||
};
|
||||
bool ok_;
|
||||
};
|
||||
|
||||
template <class E>
|
||||
class [[nodiscard]] Result<void, E> {
|
||||
public:
|
||||
using value_type = void;
|
||||
using error_type = E;
|
||||
|
||||
constexpr Result() noexcept : ok_(true), error_() {}
|
||||
|
||||
template <class G>
|
||||
constexpr Result(Unexpected<G> &&e) noexcept // NOLINT(google-explicit-constructor)
|
||||
: ok_(false), error_(std::move(e.value))
|
||||
{
|
||||
}
|
||||
|
||||
constexpr bool has_value() const noexcept { return ok_; }
|
||||
constexpr explicit operator bool() const noexcept { return ok_; }
|
||||
constexpr void value() const noexcept
|
||||
{
|
||||
if (!ok_) {
|
||||
detail::result_misuse();
|
||||
}
|
||||
}
|
||||
constexpr const E &error() const noexcept
|
||||
{
|
||||
if (ok_) {
|
||||
detail::result_misuse();
|
||||
}
|
||||
return error_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool ok_;
|
||||
E error_;
|
||||
};
|
||||
|
||||
using Status = Result<void>;
|
||||
|
||||
// Propagate an error out of a function returning any Result.
|
||||
#define SS_TRY(expr) \
|
||||
do { \
|
||||
auto ss_try_result_ = (expr); \
|
||||
if (!ss_try_result_) { \
|
||||
return ::ss::fail(ss_try_result_.error()); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
} // namespace ss
|
||||
|
||||
#endif
|
||||
@@ -57,6 +57,19 @@ function(ss_add_test name sources libs)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# C++ core (src/core): header-only ownership and allocation primitives.
|
||||
foreach(core_test result pool buffer arena fixed_map ownership)
|
||||
ss_add_test(test_core_${core_test} "test_core_${core_test}.cpp" "")
|
||||
endforeach()
|
||||
|
||||
# C++ sources under src/ must not allocate on the heap (see the script).
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME check_no_heap
|
||||
COMMAND ${Python3_EXECUTABLE} ${PROJECT_SOURCE_DIR}/scripts/check_no_heap.py --self-test)
|
||||
set_tests_properties(check_no_heap PROPERTIES LABELS "unit")
|
||||
endif()
|
||||
|
||||
# test_base64 - no external deps
|
||||
ss_add_test(test_base64
|
||||
"test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.c"
|
||||
@@ -215,6 +228,15 @@ ss_add_test(test_resolv_async
|
||||
"ss_ev;ss_cares;ss_sodium")
|
||||
set_tests_properties(test_resolv_async PROPERTIES TIMEOUT 30)
|
||||
|
||||
# Preloadable malloc counter for the allocation audit. It interposes the
|
||||
# system allocator, so it is only built where that is supported (glibc,
|
||||
# macOS) and never together with the sanitizers, which own malloc.
|
||||
include(CheckSymbolExists)
|
||||
check_symbol_exists(__GLIBC__ "stdlib.h" SS_HAVE_GLIBC)
|
||||
if(SS_BUILD_EXECUTABLES AND NOT ENABLE_SANITIZERS AND (APPLE OR SS_HAVE_GLIBC))
|
||||
add_library(ss_alloc_audit SHARED alloc_audit/alloc_counter.c)
|
||||
endif()
|
||||
|
||||
if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
|
||||
find_package(Python3 COMPONENTS Interpreter QUIET)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
@@ -233,6 +255,15 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/experiments/test_transport.py)
|
||||
set_tests_properties(test_experimental_transports PROPERTIES
|
||||
LABELS "integration" TIMEOUT 30)
|
||||
# Steady-state allocation audit (report-only until the C++ relay
|
||||
# lands; see tests/alloc_audit/measure_alloc.py).
|
||||
if(TARGET ss_alloc_audit)
|
||||
add_test(NAME alloc_audit_report
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/measure_alloc.py
|
||||
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
|
||||
--shim $<TARGET_FILE:ss_alloc_audit>)
|
||||
set_tests_properties(alloc_audit_report PROPERTIES LABELS "integration" TIMEOUT 120)
|
||||
endif()
|
||||
add_test(NAME test_printable_salt_interop
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/interop.py
|
||||
--self --printable-salt --bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#include "core/arena.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
struct Pair {
|
||||
int a;
|
||||
double b;
|
||||
};
|
||||
|
||||
void test_allocate_alignment()
|
||||
{
|
||||
ss::MonotonicArena arena;
|
||||
SS_CHECK(arena.reserve(256));
|
||||
auto c = arena.allocate(1, 1);
|
||||
SS_CHECK(c);
|
||||
auto d = arena.allocate(8, 64);
|
||||
SS_CHECK(d);
|
||||
SS_CHECK(reinterpret_cast<std::uintptr_t>(d->data()) % 64 == 0);
|
||||
auto p = arena.make<Pair>(Pair{ 3, 1.5 });
|
||||
SS_CHECK(p && (*p)->a == 3 && (*p)->b == 1.5);
|
||||
SS_CHECK(!arena.allocate(8, 3));
|
||||
}
|
||||
|
||||
void test_exhaustion_and_reset()
|
||||
{
|
||||
ss::MonotonicArena arena;
|
||||
SS_CHECK(!arena.allocate(1));
|
||||
SS_CHECK(arena.reserve(64));
|
||||
SS_CHECK(arena.allocate(64, 1));
|
||||
auto over = arena.allocate(1, 1);
|
||||
SS_CHECK(!over && over.error().code == ss::Errc::exhausted);
|
||||
arena.reset();
|
||||
SS_CHECK(arena.used() == 0 && arena.peak() == 64);
|
||||
SS_CHECK(arena.allocate(32, 1));
|
||||
}
|
||||
|
||||
void test_intern_and_arrays()
|
||||
{
|
||||
ss::MonotonicArena arena;
|
||||
SS_CHECK(arena.reserve(128));
|
||||
auto s = arena.intern("aes-256-gcm");
|
||||
SS_CHECK(s && *s == "aes-256-gcm");
|
||||
SS_CHECK(s->data()[s->size()] == '\0');
|
||||
auto empty = arena.intern("");
|
||||
SS_CHECK(empty && empty->empty());
|
||||
auto ports = arena.make_array<std::uint16_t>(4);
|
||||
SS_CHECK(ports && ports->size() == 4 && (*ports)[3] == 0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_allocate_alignment);
|
||||
SS_RUN(test_exhaustion_and_reset);
|
||||
SS_RUN(test_intern_and_arrays);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/buffer.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
ss::ConstBytes bytes(std::string_view s)
|
||||
{
|
||||
return ss::as_bytes(s.data(), s.size());
|
||||
}
|
||||
|
||||
bool equals(ss::ConstBytes b, std::string_view s)
|
||||
{
|
||||
return b.size() == s.size() && std::memcmp(b.data(), s.data(), s.size()) == 0;
|
||||
}
|
||||
|
||||
void test_append_consume()
|
||||
{
|
||||
ss::FixedBuffer<8> buf;
|
||||
SS_CHECK(buf.empty() && buf.capacity() == 8);
|
||||
SS_CHECK(buf.append(bytes("hello")));
|
||||
SS_CHECK(equals(buf.readable(), "hello"));
|
||||
SS_CHECK(buf.consume(2));
|
||||
SS_CHECK(equals(buf.readable(), "llo"));
|
||||
SS_CHECK(!buf.consume(4));
|
||||
// Appending past the tail compacts first.
|
||||
SS_CHECK(buf.append(bytes("world")));
|
||||
SS_CHECK(equals(buf.readable(), "lloworld"));
|
||||
SS_CHECK(!buf.append(bytes("x")));
|
||||
SS_CHECK(buf.consume(8) && buf.empty());
|
||||
}
|
||||
|
||||
void test_commit_writable()
|
||||
{
|
||||
ss::FixedBuffer<4> buf;
|
||||
auto w = buf.writable();
|
||||
SS_CHECK(w.size() == 4);
|
||||
std::memcpy(w.data(), "abcd", 4);
|
||||
SS_CHECK(buf.commit(4));
|
||||
SS_CHECK(buf.full() && buf.writable().empty());
|
||||
SS_CHECK(!buf.commit(1));
|
||||
SS_CHECK(equals(buf.readable(), "abcd"));
|
||||
}
|
||||
|
||||
void test_prepend()
|
||||
{
|
||||
ss::FixedBuffer<10> buf;
|
||||
SS_CHECK(buf.append(bytes("data")));
|
||||
// No headroom: payload shifts right.
|
||||
SS_CHECK(buf.prepend(bytes("hdr")));
|
||||
SS_CHECK(equals(buf.readable(), "hdrdata"));
|
||||
SS_CHECK(buf.consume(3));
|
||||
// Headroom from the consumed bytes is reused in place.
|
||||
SS_CHECK(buf.prepend(bytes("ab")));
|
||||
SS_CHECK(equals(buf.readable(), "abdata"));
|
||||
SS_CHECK(!buf.prepend(bytes("12345")));
|
||||
SS_CHECK(equals(buf.readable(), "abdata"));
|
||||
}
|
||||
|
||||
void test_assign_wipe()
|
||||
{
|
||||
ss::FixedBuffer<4> buf;
|
||||
SS_CHECK(!buf.assign(bytes("toolong")));
|
||||
SS_CHECK(buf.assign(bytes("key")));
|
||||
SS_CHECK(equals(buf.readable(), "key"));
|
||||
buf.wipe();
|
||||
SS_CHECK(buf.empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_append_consume);
|
||||
SS_RUN(test_commit_writable);
|
||||
SS_RUN(test_prepend);
|
||||
SS_RUN(test_assign_wipe);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "core/fixed_map.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using Key = ss::FixedKey<16>;
|
||||
|
||||
Key key(std::string_view s)
|
||||
{
|
||||
auto k = Key::from(ss::as_bytes(s.data(), s.size()));
|
||||
SS_CHECK(k.has_value());
|
||||
return *k;
|
||||
}
|
||||
|
||||
// Every key lands in the same home slot, to exercise probe runs and the
|
||||
// backward-shift deletion.
|
||||
struct CollidingHash {
|
||||
std::uint64_t operator()(std::uint32_t, std::uint64_t) const noexcept { return 7; }
|
||||
};
|
||||
|
||||
void test_map_basic()
|
||||
{
|
||||
ss::FixedHashMap<std::uint32_t, int> map;
|
||||
SS_CHECK(map.reserve(3, 0x1234));
|
||||
SS_CHECK(map.emplace(1, 10));
|
||||
SS_CHECK(map.emplace(2, 20));
|
||||
SS_CHECK(map.emplace(3, 30));
|
||||
auto dup = map.emplace(2, 99);
|
||||
SS_CHECK(!dup && dup.error().code == ss::Errc::invalid);
|
||||
auto over = map.emplace(4, 40);
|
||||
SS_CHECK(!over && over.error().code == ss::Errc::exhausted);
|
||||
SS_CHECK(*map.find(2) == 20);
|
||||
auto taken = map.take(2);
|
||||
SS_CHECK(taken && *taken == 20);
|
||||
SS_CHECK(!map.find(2) && map.size() == 2);
|
||||
SS_CHECK(map.emplace(4, 40));
|
||||
SS_CHECK(!map.erase(5));
|
||||
}
|
||||
|
||||
void test_map_backward_shift()
|
||||
{
|
||||
ss::FixedHashMap<std::uint32_t, int, CollidingHash> map;
|
||||
SS_CHECK(map.reserve(8));
|
||||
for (std::uint32_t i = 0; i < 8; ++i) {
|
||||
SS_CHECK(map.emplace(i, static_cast<int>(i) * 100));
|
||||
}
|
||||
// Remove from the middle of the probe run; later entries must still be
|
||||
// reachable without tombstones.
|
||||
SS_CHECK(map.erase(3));
|
||||
SS_CHECK(map.erase(0));
|
||||
for (std::uint32_t i = 0; i < 8; ++i) {
|
||||
const int *v = map.find(i);
|
||||
if (i == 0 || i == 3) {
|
||||
SS_CHECK(v == nullptr);
|
||||
} else {
|
||||
SS_CHECK(v != nullptr && *v == static_cast<int>(i) * 100);
|
||||
}
|
||||
}
|
||||
int count = 0;
|
||||
map.for_each([&](std::uint32_t, int &) { ++count; });
|
||||
SS_CHECK(count == 6);
|
||||
}
|
||||
|
||||
void test_map_move_only_values()
|
||||
{
|
||||
ss::FixedHashMap<std::uint32_t, std::unique_ptr<int> > map;
|
||||
SS_CHECK(map.reserve(2));
|
||||
SS_CHECK(map.emplace(1, std::make_unique<int>(5)));
|
||||
auto v = map.take(1);
|
||||
SS_CHECK(v && **v == 5);
|
||||
}
|
||||
|
||||
void test_lru_order()
|
||||
{
|
||||
ss::FixedLru<Key, int> lru;
|
||||
SS_CHECK(lru.reserve(3, 42));
|
||||
SS_CHECK(lru.emplace(key("a"), 1));
|
||||
SS_CHECK(lru.emplace(key("b"), 2));
|
||||
SS_CHECK(lru.emplace(key("c"), 3));
|
||||
SS_CHECK(lru.full());
|
||||
auto over = lru.emplace(key("d"), 4);
|
||||
SS_CHECK(!over && over.error().code == ss::Errc::exhausted);
|
||||
|
||||
// Touch "a" so "b" becomes the oldest.
|
||||
SS_CHECK(*lru.find(key("a")) == 1);
|
||||
SS_CHECK(*lru.oldest_key() == key("b"));
|
||||
// peek() does not change the order.
|
||||
SS_CHECK(*lru.peek(key("b")) == 2);
|
||||
SS_CHECK(*lru.oldest_key() == key("b"));
|
||||
|
||||
auto evicted = lru.pop_oldest();
|
||||
SS_CHECK(evicted && evicted->first == key("b") && evicted->second == 2);
|
||||
SS_CHECK(lru.emplace(key("d"), 4));
|
||||
|
||||
int order[3] = {};
|
||||
int n = 0;
|
||||
lru.for_each([&](const Key &, int &v) { order[n++] = v; });
|
||||
SS_CHECK(n == 3 && order[0] == 3 && order[1] == 1 && order[2] == 4);
|
||||
}
|
||||
|
||||
void test_lru_stable_values()
|
||||
{
|
||||
ss::FixedLru<std::uint32_t, int> lru;
|
||||
SS_CHECK(lru.reserve(64));
|
||||
auto first = lru.emplace(0, 1000);
|
||||
SS_CHECK(first);
|
||||
int *stable = *first;
|
||||
for (std::uint32_t i = 1; i < 64; ++i) {
|
||||
SS_CHECK(lru.emplace(i, static_cast<int>(i)));
|
||||
}
|
||||
for (std::uint32_t i = 1; i < 64; i += 2) {
|
||||
SS_CHECK(lru.erase(i));
|
||||
}
|
||||
// Erasing others reshuffles the index but never moves values.
|
||||
SS_CHECK(lru.peek(0) == stable && *stable == 1000);
|
||||
auto taken = lru.take(0);
|
||||
SS_CHECK(taken && *taken == 1000);
|
||||
SS_CHECK(lru.size() == 31);
|
||||
lru.clear();
|
||||
SS_CHECK(lru.empty() && lru.oldest() == nullptr);
|
||||
}
|
||||
|
||||
void test_key_limits()
|
||||
{
|
||||
char big[17] = {};
|
||||
SS_CHECK(!Key::from(ss::as_bytes(big, sizeof(big))));
|
||||
SS_CHECK(key("ab") != key("abc"));
|
||||
SS_CHECK(key("") == key(""));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_map_basic);
|
||||
SS_RUN(test_map_backward_shift);
|
||||
SS_RUN(test_map_move_only_values);
|
||||
SS_RUN(test_lru_order);
|
||||
SS_RUN(test_lru_stable_values);
|
||||
SS_RUN(test_key_limits);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
// IntrusiveList, RefPtr and FixedHeap: the remaining ownership primitives.
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
#include "core/fixed_heap.hpp"
|
||||
#include "core/intrusive_list.hpp"
|
||||
#include "core/pool.hpp"
|
||||
#include "core/ref.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
struct IdleTag;
|
||||
|
||||
struct Conn : ss::ListNode<>, ss::ListNode<IdleTag> {
|
||||
explicit Conn(int v) noexcept : id(v) {}
|
||||
int id;
|
||||
};
|
||||
|
||||
void test_list_basic()
|
||||
{
|
||||
ss::IntrusiveList<Conn> all;
|
||||
ss::IntrusiveList<Conn, IdleTag> idle;
|
||||
Conn a(1), b(2), c(3);
|
||||
all.push_back(a);
|
||||
all.push_back(b);
|
||||
all.push_front(c);
|
||||
idle.push_back(b);
|
||||
SS_CHECK(all.size() == 3 && idle.size() == 1);
|
||||
SS_CHECK(all.front()->id == 3 && all.back()->id == 2);
|
||||
|
||||
all.move_to_back(c);
|
||||
SS_CHECK(all.back()->id == 3);
|
||||
|
||||
// Being on one list does not affect the other.
|
||||
ss::IntrusiveList<Conn>::remove(b);
|
||||
SS_CHECK(all.size() == 2 && idle.front() == &b);
|
||||
|
||||
int sum = 0;
|
||||
all.for_each([&](Conn &x) { sum += x.id; });
|
||||
SS_CHECK(sum == 4);
|
||||
SS_CHECK(all.pop_front()->id == 1);
|
||||
}
|
||||
|
||||
void test_list_unlinks_on_destruction()
|
||||
{
|
||||
ss::IntrusiveList<Conn> all;
|
||||
{
|
||||
Conn temp(9);
|
||||
all.push_back(temp);
|
||||
SS_CHECK(!all.empty());
|
||||
}
|
||||
SS_CHECK(all.empty());
|
||||
}
|
||||
|
||||
// A pooled, shared object: the pool owns the storage, references own the
|
||||
// object's lifetime.
|
||||
struct Session : ss::RefCounted<Session> {
|
||||
static inline ss::ObjectPool<Session> *pool = nullptr;
|
||||
static inline int destroyed = 0;
|
||||
|
||||
// The PoolPtr is parked inside the object while references exist.
|
||||
ss::PoolPtr<Session> self;
|
||||
|
||||
static void destroy(Session *s) noexcept
|
||||
{
|
||||
++destroyed;
|
||||
ss::PoolPtr<Session> owner = std::move(s->self);
|
||||
owner.reset(); // returns the slot to the pool
|
||||
}
|
||||
};
|
||||
|
||||
ss::RefPtr<Session> make_session()
|
||||
{
|
||||
auto slot = Session::pool->acquire();
|
||||
SS_CHECK(slot);
|
||||
Session *raw = slot->get();
|
||||
raw->self = std::move(slot).value();
|
||||
return ss::RefPtr<Session>(raw);
|
||||
}
|
||||
|
||||
void test_refptr_lifecycle()
|
||||
{
|
||||
ss::ObjectPool<Session> pool;
|
||||
SS_CHECK(pool.reserve(2));
|
||||
Session::pool = &pool;
|
||||
Session::destroyed = 0;
|
||||
{
|
||||
ss::RefPtr<Session> table = make_session();
|
||||
SS_CHECK(table->ref_count() == 1);
|
||||
ss::RefPtr<Session> relay = table;
|
||||
SS_CHECK(table->ref_count() == 2);
|
||||
ss::RefPtr<Session> moved = std::move(relay);
|
||||
SS_CHECK(!relay && moved->ref_count() == 2); // NOLINT(bugprone-use-after-move): checks moved-from state
|
||||
table.reset();
|
||||
SS_CHECK(Session::destroyed == 0 && pool.live() == 1);
|
||||
moved = nullptr;
|
||||
SS_CHECK(Session::destroyed == 1 && pool.live() == 0);
|
||||
}
|
||||
SS_CHECK(pool.available() == 2);
|
||||
}
|
||||
|
||||
void test_fixed_heap()
|
||||
{
|
||||
ss::FixedHeap heap;
|
||||
SS_CHECK(heap.reserve(4096));
|
||||
void *a = heap.allocate(10);
|
||||
void *b = heap.allocate_zeroed(4, 25);
|
||||
SS_CHECK(a && b && heap.owns(a) && heap.owns(b));
|
||||
SS_CHECK(reinterpret_cast<std::uintptr_t>(a) % 16 == 0);
|
||||
auto *zeros = static_cast<unsigned char *>(b);
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
SS_CHECK(zeros[i] == 0);
|
||||
}
|
||||
std::memcpy(a, "0123456789", 10);
|
||||
void *grown = heap.reallocate(a, 200);
|
||||
SS_CHECK(grown && std::memcmp(grown, "0123456789", 10) == 0);
|
||||
|
||||
auto before = heap.stats();
|
||||
heap.deallocate(grown);
|
||||
heap.deallocate(b);
|
||||
SS_CHECK(heap.stats().in_use < before.in_use);
|
||||
|
||||
// Freed blocks are reused instead of consuming fresh space.
|
||||
void *again = heap.allocate(200);
|
||||
SS_CHECK(again == grown);
|
||||
heap.deallocate(again);
|
||||
|
||||
SS_CHECK(heap.allocate(ss::FixedHeap::max_request() + 1) == nullptr);
|
||||
SS_CHECK(heap.allocate(8192) == nullptr); // over budget
|
||||
SS_CHECK(heap.stats().failures == 2);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_list_basic);
|
||||
SS_RUN(test_list_unlinks_on_destruction);
|
||||
SS_RUN(test_refptr_lifecycle);
|
||||
SS_RUN(test_fixed_heap);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include "core/pool.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
int g_destroyed = 0;
|
||||
|
||||
struct Plain {
|
||||
explicit Plain(int v) noexcept : value(v) {}
|
||||
~Plain() { ++g_destroyed; }
|
||||
int value;
|
||||
};
|
||||
|
||||
// Models an object that owns libuv handles: it can only be recycled once
|
||||
// its asynchronous closes have finished.
|
||||
struct Closable {
|
||||
static inline ss::CloseCompletion pending;
|
||||
static inline int begun = 0;
|
||||
|
||||
void begin_close(ss::CloseCompletion done) noexcept
|
||||
{
|
||||
++begun;
|
||||
pending = done;
|
||||
}
|
||||
~Closable() { ++g_destroyed; }
|
||||
};
|
||||
|
||||
struct ClosesSynchronously {
|
||||
void begin_close(ss::CloseCompletion done) noexcept { done(); }
|
||||
};
|
||||
|
||||
void test_acquire_release()
|
||||
{
|
||||
ss::ObjectPool<Plain> pool;
|
||||
SS_CHECK(pool.reserve(2));
|
||||
g_destroyed = 0;
|
||||
{
|
||||
auto a = pool.acquire(1);
|
||||
auto b = pool.acquire(2);
|
||||
SS_CHECK(a && b);
|
||||
SS_CHECK((*a)->value == 1 && (*b)->value == 2);
|
||||
SS_CHECK(pool.live() == 2 && pool.full());
|
||||
|
||||
auto c = pool.acquire(3);
|
||||
SS_CHECK(!c && c.error().code == ss::Errc::exhausted);
|
||||
}
|
||||
SS_CHECK(g_destroyed == 2);
|
||||
SS_CHECK(pool.live() == 0 && pool.available() == 2);
|
||||
}
|
||||
|
||||
void test_reuse_and_handles()
|
||||
{
|
||||
ss::ObjectPool<Plain> pool;
|
||||
SS_CHECK(pool.reserve(1));
|
||||
auto first = pool.acquire(10);
|
||||
SS_CHECK(first);
|
||||
ss::PoolPtr<Plain> owner = std::move(first).value();
|
||||
ss::Handle<Plain> h = pool.handle_of(owner.get());
|
||||
SS_CHECK(h);
|
||||
SS_CHECK(pool.resolve(h) == owner.get());
|
||||
|
||||
owner.reset();
|
||||
SS_CHECK(pool.resolve(h) == nullptr);
|
||||
|
||||
// The slot is reused, but the stale handle must not resolve to the new
|
||||
// occupant.
|
||||
auto second = pool.acquire(20);
|
||||
SS_CHECK(second);
|
||||
SS_CHECK(pool.resolve(h) == nullptr);
|
||||
SS_CHECK(pool.resolve(pool.handle_of(second->get())) == second->get());
|
||||
SS_CHECK(!pool.resolve(ss::Handle<Plain>{}));
|
||||
}
|
||||
|
||||
void test_async_close()
|
||||
{
|
||||
ss::ObjectPool<Closable> pool;
|
||||
SS_CHECK(pool.reserve(1));
|
||||
g_destroyed = 0;
|
||||
Closable::begun = 0;
|
||||
auto obj = pool.acquire();
|
||||
SS_CHECK(obj);
|
||||
ss::Handle<Closable> h = pool.handle_of(obj->get());
|
||||
|
||||
obj->reset();
|
||||
// Ownership is gone, but the memory is not reusable yet.
|
||||
SS_CHECK(Closable::begun == 1);
|
||||
SS_CHECK(g_destroyed == 0);
|
||||
SS_CHECK(pool.closing() == 1 && pool.available() == 0);
|
||||
SS_CHECK(pool.resolve(h) == nullptr);
|
||||
SS_CHECK(!pool.acquire());
|
||||
|
||||
Closable::pending();
|
||||
SS_CHECK(g_destroyed == 1);
|
||||
SS_CHECK(pool.closing() == 0 && pool.available() == 1);
|
||||
{
|
||||
auto again = pool.acquire();
|
||||
SS_CHECK(again);
|
||||
}
|
||||
// The pool refuses to die with a close still in flight, so finish it.
|
||||
SS_CHECK(pool.closing() == 1);
|
||||
Closable::pending();
|
||||
SS_CHECK(pool.closing() == 0);
|
||||
}
|
||||
|
||||
void test_sync_close()
|
||||
{
|
||||
ss::ObjectPool<ClosesSynchronously> pool;
|
||||
SS_CHECK(pool.reserve(1));
|
||||
{
|
||||
auto obj = pool.acquire();
|
||||
SS_CHECK(obj);
|
||||
}
|
||||
SS_CHECK(pool.available() == 1);
|
||||
}
|
||||
|
||||
void test_reserve_rules()
|
||||
{
|
||||
ss::ObjectPool<Plain> pool;
|
||||
SS_CHECK(!pool.acquire(1));
|
||||
SS_CHECK(!pool.reserve(0));
|
||||
SS_CHECK(pool.reserve(4));
|
||||
SS_CHECK(!pool.reserve(4));
|
||||
SS_CHECK(pool.capacity() == 4);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_acquire_release);
|
||||
SS_RUN(test_reuse_and_handles);
|
||||
SS_RUN(test_async_close);
|
||||
SS_RUN(test_sync_close);
|
||||
SS_RUN(test_reserve_rules);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "core/result.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
ss::Result<int> parse_positive(int v)
|
||||
{
|
||||
if (v <= 0) {
|
||||
return ss::fail(ss::Errc::invalid);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
ss::Status check_twice(int v)
|
||||
{
|
||||
SS_TRY(parse_positive(v));
|
||||
SS_TRY(parse_positive(v - 1));
|
||||
return {};
|
||||
}
|
||||
|
||||
void test_value_and_error()
|
||||
{
|
||||
auto ok = parse_positive(5);
|
||||
SS_CHECK(ok.has_value());
|
||||
SS_CHECK(*ok == 5);
|
||||
SS_CHECK(ok.value_or(0) == 5);
|
||||
|
||||
auto bad = parse_positive(-1);
|
||||
SS_CHECK(!bad);
|
||||
SS_CHECK(bad.error().code == ss::Errc::invalid);
|
||||
SS_CHECK(bad.value_or(42) == 42);
|
||||
}
|
||||
|
||||
void test_status_and_try()
|
||||
{
|
||||
SS_CHECK(check_twice(3).has_value());
|
||||
auto r = check_twice(1);
|
||||
SS_CHECK(!r);
|
||||
SS_CHECK(r.error() == ss::Error{ ss::Errc::invalid, 0 });
|
||||
}
|
||||
|
||||
void test_move_only_value()
|
||||
{
|
||||
auto make = []() -> ss::Result<std::unique_ptr<int> > { return std::make_unique<int>(7); };
|
||||
auto r = make();
|
||||
SS_CHECK(r);
|
||||
std::unique_ptr<int> p = std::move(r).value();
|
||||
SS_CHECK(*p == 7);
|
||||
}
|
||||
|
||||
void test_monadic()
|
||||
{
|
||||
auto doubled = parse_positive(4).transform([](int v) { return v * 2; });
|
||||
SS_CHECK(doubled && *doubled == 8);
|
||||
auto chained = parse_positive(4).and_then([](int v) { return parse_positive(v - 10); });
|
||||
SS_CHECK(!chained && chained.error().code == ss::Errc::invalid);
|
||||
}
|
||||
|
||||
void test_sys_errno()
|
||||
{
|
||||
ss::Result<int> r = ss::fail(ss::Errc::io, 104);
|
||||
SS_CHECK(r.error().sys == 104);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_value_and_error);
|
||||
SS_RUN(test_status_and_try);
|
||||
SS_RUN(test_move_only_value);
|
||||
SS_RUN(test_monadic);
|
||||
SS_RUN(test_sys_errno);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef SS_TEST_HELPERS_HPP
|
||||
#define SS_TEST_HELPERS_HPP
|
||||
|
||||
// Minimal harness for the C++ unit tests. Tests are built without
|
||||
// exceptions, so a failed check reports its location and aborts.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#define SS_CHECK(...) \
|
||||
do { \
|
||||
if (!(__VA_ARGS__)) { \
|
||||
std::fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, \
|
||||
__LINE__, #__VA_ARGS__); \
|
||||
std::abort(); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define SS_RUN(test) \
|
||||
do { \
|
||||
test(); \
|
||||
std::printf("ok %s\n", #test); \
|
||||
} while (0)
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user