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.
4.9 KiB
C++20 core and allocation policy
Status: phase 1 of an incremental port. The ownership and allocation
primitives in src/core/ exist and are tested; no program code uses them yet.
All binaries are still built from the C sources.
Goals
- Move
src/from C11 to C++20, one module at a time, with every step keeping the CLI, the JSON configuration, protocol behaviour and the publicshadowsocks.hC ABI unchanged. - Make ownership explicit. Each object has exactly one owner, a
std::unique_ptr. Non-owning references are generation-checked handles that go stale instead of dangling. - No heap allocation after startup. Connections, buffers, cipher contexts,
DNS queries and UDP sessions come from fixed-capacity pools reserved once
at startup. The steady-state data path does not call
malloc. When a pool is full, new work is refused rather than allocated.
Language profile
- C++20,
-fno-exceptions -fno-rtti -fno-threadsafe-statics, warnings as errors. Errors are returned asss::Result<T>(src/core/result.hpp), which implements a subset ofstd::expected. - Only header-only parts of the standard library are used (
<array>,<span>,<optional>,<variant>,<memory>'sstd::unique_ptr). This keeps the binaries and the static library free of libstdc++/libc++ symbols, so C consumers oflibshadowsocks-c.ado not need a C++ runtime. scripts/check_no_heap.py(a ctest) rejectsnew,malloc, allocating containers,std::string,std::functionandstd::shared_ptrin C++ sources undersrc/. A startup reservation that really must allocate is marked// heap-ok: <reason>.- blake3 (
src/blake3) andthird_party/bloomstay C. - Minimum compilers: GCC 11, Clang 14, Apple Clang 15.
Primitives (src/core/)
| Header | Purpose |
|---|---|
result.hpp |
Result<T>, Status, Errc, SS_TRY. |
region.hpp |
Region, the only startup calloc in the core. Also ASan poison helpers. |
pool.hpp |
ObjectPool<T>, PoolPtr<T> (std::unique_ptr with a pool deleter), Handle<T>. Types that embed libuv handles model AsyncClosable: their slot is recycled only after the asynchronous close finishes. |
ref.hpp |
RefCounted<T>/RefPtr<T>, intrusive shared ownership for the few genuinely shared objects (AEAD-2022 UDP sessions). Used instead of std::shared_ptr because its control block cannot be pooled portably. |
buffer.hpp |
FixedBuffer<N>: inline bytes with std::span views; replaces buffer_t. |
arena.hpp |
MonotonicArena for configuration data that lives as long as the process. |
fixed_map.hpp |
FixedHashMap (open addressing, backward-shift deletion) and FixedLru; replace uthash and cache.c. |
intrusive_list.hpp |
IntrusiveList/ListNode; replaces ss_list. Nodes unlink themselves when destroyed. |
fixed_heap.hpp |
Bounded size-class allocator to hand to libuv, c-ares and Mbed TLS through their allocator hooks. |
Phases
- Build and core (this change): C++ enabled in CMake,
src/core/, unit tests (test_core_*), the no-heap lint and the allocation audit. - Mechanical C-to-C++ conversion, leaf modules first.
- Event loop wrapper over libuv.
- Crypto: cipher contexts held by value, dispatched with
std::variant. - Services: cache, lists, resolver, ACL and configuration on the new primitives.
- One TCP relay engine shared by ss-tunnel, ss-local, ss-redir and ss-server.
- UDP relay engine.
- Seal the heap after startup; new
max_connections,max_udp_sessionsandmax_dns_queriessettings size the pools. - Public API:
shadowsocks.hkept byte-for-byte, plus a new installed C++ header with an RAII handle. - Remove the C shims.
Allocation audit
tests/alloc_audit/alloc_counter.c is a preloadable malloc counter
(glibc and macOS). tests/alloc_audit/measure_alloc.py warms ss-local and
ss-server up, snapshots their counters, relays more connections and reports
heap allocations per connection. It runs as the alloc_audit_report ctest
(report-only for now). It becomes a hard check, with a budget of zero, once
the relay is ported.
python3 tests/alloc_audit/measure_alloc.py --bin build/bin \
--shim build/lib/libss_alloc_audit.dylib --method aes-256-gcm
C baseline
Commit 553cdc7, macOS arm64, bundled Release build, 200 connections, each
echoing 16 KiB through ss-local and ss-server on loopback. The figures
include allocations made inside libuv, c-ares and Mbed TLS. ss-server
resolves the destination (localhost) for every connection, so its count
includes a DNS lookup.
| Method | ss-server allocations/connection | ss-local allocations/connection |
|---|---|---|
| aes-256-gcm | 89.1 | 39.2 |
| chacha20-ietf-poly1305 | 83.1 | 33.0 |
| 2022-blake3-aes-256-gcm | 84.0 | 33.0 |
| aes-256-cfb (stream) | 77.1 | 26.5 |
| aes-256-gcm, 1-byte payload | 85.0 | 36.0 |
Unstripped Release sizes at the same commit: ss-local 1,086,888 bytes, ss-server 1,086,952 bytes, ss-tunnel 542,200 bytes.