mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-20 13:03:50 +00:00
* 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.
146 lines
3.9 KiB
C++
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;
|
|
}
|