mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-21 22:43:48 +00:00
* Make headers and leaf modules compile as C++
Wrap the declarations of every project header in extern "C" (after the
system includes, which must keep C++ linkage), so converted modules and
the remaining C files agree on linkage.
In the leaf modules, cast allocator and memchr results explicitly,
rename identifiers called new, replace the getpwnam_r VLA with the
fixed 16 KiB buffer the loop already capped itself at, and make the
rlimit initialiser conversion explicit. All of this is still valid C.
* Compile the leaf modules as C++
Rename base64, json, ipset, process, ppbloom, rule, ssurl and cache from
.c to .cpp without touching their contents, and update the CMake source
lists, debian/copyright and the docs.
Ported code keeps -Wall; -Wextra is now opt-in through
ss_strict_warnings() for code written as C++ (the core tests).
check_no_heap.py gains a ratchet baseline for the converted modules,
which still allocate the way the C code did. A new check_cxx_runtime
test verifies with nm that the programs and libshadowsocks-c.a still
need no C++ runtime library, so C consumers are unaffected.
* Fix the Windows branches of process.cpp for C++
Cast allocator results and the thread argument explicitly and
value-initialise JOBOBJECT_EXTENDED_LIMIT_INFORMATION with {}. Found by
the zig x86_64-windows-gnu cross build.
* Keep clang-tidy clean for the converted modules
Once the C headers are included from C++ translation units,
bugprone-dynamic-static-initializers reports each extern variable
declaration in them; none is defined in a header, so disable the check.
Mark the two bounded memchr searches in ssurl.cpp, which
bugprone-not-null-terminated-result misreads as string copies.
* check_cxx_runtime: ignore glibc symbol versions
nm on glibc prints __cxa_atexit@GLIBC_2.2.5, which the allowlist for the
libc-provided __cxa_atexit/__cxa_finalize did not match.
* Keep GCC's C++-only warnings off for modules converted from C
GCC's C++ -Wall includes -Wsign-compare and -Wclass-memaccess, which C's
-Wall does not, so the converted modules failed -Werror on GCC (Linux,
UCRT64) although nothing in them changed. They keep the warning set they
were written against; code under ss_strict_warnings() re-enables both.
98 lines
4.9 KiB
Markdown
98 lines
4.9 KiB
Markdown
# 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 public
|
|
`shadowsocks.h` C 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 as `ss::Result<T>` (`src/core/result.hpp`), which
|
|
implements a subset of `std::expected`.
|
|
- Only header-only parts of the standard library are used (`<array>`,
|
|
`<span>`, `<optional>`, `<variant>`, `<memory>`'s `std::unique_ptr`). This
|
|
keeps the binaries and the static library free of libstdc++/libc++ symbols,
|
|
so C consumers of `libshadowsocks-c.a` do not need a C++ runtime.
|
|
- `scripts/check_no_heap.py` (a ctest) rejects `new`, `malloc`, allocating
|
|
containers, `std::string`, `std::function` and `std::shared_ptr` in C++
|
|
sources under `src/`. A startup reservation that really must allocate is
|
|
marked `// heap-ok: <reason>`.
|
|
- blake3 (`src/blake3`) and `third_party/bloom` stay 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.cpp`. |
|
|
| `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
|
|
|
|
1. Build and core (this change): C++ enabled in CMake, `src/core/`, unit tests
|
|
(`test_core_*`), the no-heap lint and the allocation audit.
|
|
2. Mechanical C-to-C++ conversion, leaf modules first.
|
|
3. Event loop wrapper over libuv.
|
|
4. Crypto: cipher contexts held by value, dispatched with `std::variant`.
|
|
5. Services: cache, lists, resolver, ACL and configuration on the new primitives.
|
|
6. One TCP relay engine shared by ss-tunnel, ss-local, ss-redir and ss-server.
|
|
7. UDP relay engine.
|
|
8. Seal the heap after startup; new `max_connections`, `max_udp_sessions`
|
|
and `max_dns_queries` settings size the pools.
|
|
9. Public API: `shadowsocks.h` kept byte-for-byte, plus a new installed C++
|
|
header with an RAII handle.
|
|
10. 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.
|
|
|
|
```sh
|
|
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.
|