Files
shadowsocks-libev/tests/test_core_ownership.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

146 lines
3.9 KiB
C++

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