Files
shadowsocks-libev/tests/test_core_pool.cpp
T
Max Lv bcb3731968 C++20 port 1/14: build, ownership core and allocation audit (#3065)
* Enable C++20 alongside C11 in the build

Add CXX to the project with C++20 (no extensions) and build C++ sources
with -Wextra -fno-exceptions -fno-rtti -fno-threadsafe-statics on top of
the existing warning flags. Give every toolchain a C++ compiler: the zig
Windows toolchain, the static macOS/Windows CI jobs, the static musl
image (clang++ plus libstdc++ headers) and the RPM spec.

* 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.

* Add heap allocation audit for relayed connections

alloc_counter is a preloadable malloc counter (glibc, macOS) that writes
a snapshot on SIGUSR2. measure_alloc.py warms ss-local and ss-server up,
snapshots both, relays more connections and reports allocations per
connection. It runs report-only as the alloc_audit_report ctest.
interop.peers() now yields the started processes.

* Document the C++20 port and allocation baseline

docs/cpp-core.md describes the language profile, the core primitives,
the phase plan and the measured C baseline (77-89 allocations per
connection in ss-server, 27-39 in ss-local). The compatibility contract
now keeps the C11 public header instead of a C11 code base. CLAUDE.md
drops stale libev, submodule and gnu99 references.

* Fix CI for the C++ core: libstdc++ headers, static unwinder, newer clang

- docker/static: Alpine's libstdc++ headers come with g++, and the C++
  tests' static libstdc++ needs an unwinder that compiler-rt lacks; add
  g++ and llvm-libunwind.
- FixedBuffer: drop the unused kCapacity (clang-tidy 18 reports it as a
  dynamically initialized header static).
- ss-server on Windows: manager_addr was only read in POSIX-only code,
  which newer clang rejects (-Wunused-but-set-global); say that the
  option is ignored there.

* CI: allow running the test and build workflows by hand

Stacked PRs target their parent branch, which these workflows do not
trigger on; workflow_dispatch lets each layer run the full matrix.

* CI: let clang-tidy ignore GCC-only warning flags

The compile database comes from a GCC build, whose flags (for example
-Wno-class-memaccess) clang-tidy does not know and reports as errors.
2026-10-15 09:45:54 +08:00

141 lines
3.5 KiB
C++

/* 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;
}