mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-11-01 15:33:50 +00:00
C++20 port 2/14: compile the leaf modules as C++ (#3066)
* 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.
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Fail if programs or libraries need the C++ runtime library.
|
||||
|
||||
The C++ core is built without exceptions or RTTI and uses only header-only
|
||||
parts of the standard library, so the programs and libshadowsocks-c stay
|
||||
consumable from plain C: a C program linking libshadowsocks-c.a must not need
|
||||
libstdc++ or libc++. This script lists the undefined symbols of each file with
|
||||
nm and rejects anything served by the C++ runtime (operator new/delete,
|
||||
exception and RTTI support, std:: functions).
|
||||
|
||||
Usage: check_cxx_runtime.py [--nm NM] FILE...
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Itanium-ABI names (with or without the Mach-O leading underscore) that only
|
||||
# the C++ runtime defines.
|
||||
RUNTIME = re.compile(
|
||||
r"^_?(?:"
|
||||
r"_Zn[wa]|_Zd[la]|" # operator new / delete
|
||||
r"_ZSt|_ZNSt|_ZNKSt|" # std:: functions and members
|
||||
r"_ZTI|_ZTS|_ZTV|" # typeinfo / vtables (RTTI, classes from the runtime)
|
||||
r"__cxa_(?!atexit$|finalize$|thread_atexit_impl$)|" # exception/guard support; libc owns these three
|
||||
r"__gxx_personality|_Unwind_|__dynamic_cast"
|
||||
r")"
|
||||
)
|
||||
|
||||
|
||||
def undefined_symbols(nm, path):
|
||||
result = subprocess.run([nm, "-u", path], capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
raise SystemExit(f"{nm} failed on {path}: {result.stderr.strip()}")
|
||||
symbols = set()
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.endswith(":"):
|
||||
continue # archive member headers
|
||||
# glibc nm prints versioned names (__cxa_atexit@GLIBC_2.2.5).
|
||||
symbols.add(line.split()[-1].split("@")[0])
|
||||
return symbols
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--nm", default=shutil.which("nm") or "nm")
|
||||
parser.add_argument("files", nargs="+")
|
||||
args = parser.parse_args()
|
||||
failed = False
|
||||
for path in args.files:
|
||||
bad = sorted(s for s in undefined_symbols(args.nm, path) if RUNTIME.match(s))
|
||||
if bad:
|
||||
failed = True
|
||||
print(f"{path}: needs the C++ runtime:")
|
||||
for symbol in bad:
|
||||
print(f" {symbol}")
|
||||
else:
|
||||
print(f"{path}: ok")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -14,7 +14,12 @@ A line that must allocate (a startup reservation) carries a justification:
|
||||
|
||||
void *base = std::calloc(1, size); // heap-ok: startup reservation
|
||||
|
||||
Usage: check_no_heap.py [--root DIR] [--self-test]
|
||||
Modules converted mechanically from C still allocate the way the C code did
|
||||
until they are rewritten. scripts/no_heap_baseline.txt lists them with their
|
||||
current violation counts, and works as a ratchet: a file may not gain
|
||||
violations, and when it loses some the baseline must be lowered to match.
|
||||
|
||||
Usage: check_no_heap.py [--root DIR] [--baseline FILE] [--self-test]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
@@ -89,16 +94,42 @@ def check_text(text):
|
||||
return problems
|
||||
|
||||
|
||||
def check_tree(root):
|
||||
def read_baseline(path):
|
||||
baseline = {}
|
||||
if path is None or not path.exists():
|
||||
return baseline
|
||||
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw.split("#", 1)[0].strip()
|
||||
if line:
|
||||
name, count = line.split()
|
||||
baseline[name] = int(count)
|
||||
return baseline
|
||||
|
||||
|
||||
def check_tree(root, baseline):
|
||||
src = root / "src"
|
||||
files = sorted(p for p in src.rglob("*") if p.suffix in (".cpp", ".hpp") and "blake3" not in p.parts)
|
||||
failures = 0
|
||||
ok = True
|
||||
total = 0
|
||||
for path in files:
|
||||
for number, why, line in check_text(path.read_text(encoding="utf-8")):
|
||||
print(f"{path.relative_to(root)}:{number}: {why}: {line}")
|
||||
failures += 1
|
||||
print(f"check_no_heap: {len(files)} C++ files, {failures} violation(s)")
|
||||
return failures == 0
|
||||
name = path.relative_to(root).as_posix()
|
||||
problems = check_text(path.read_text(encoding="utf-8"))
|
||||
allowed = baseline.pop(name, 0)
|
||||
total += len(problems)
|
||||
if len(problems) > allowed:
|
||||
for number, why, line in problems:
|
||||
print(f"{name}:{number}: {why}: {line}")
|
||||
print(f"{name}: {len(problems)} violation(s), baseline allows {allowed}")
|
||||
ok = False
|
||||
elif len(problems) < allowed:
|
||||
print(f"{name}: {len(problems)} violation(s), below the baseline of {allowed}; "
|
||||
"lower it in scripts/no_heap_baseline.txt")
|
||||
ok = False
|
||||
for name in sorted(baseline):
|
||||
print(f"{name}: listed in the baseline but not a C++ source; remove the entry")
|
||||
ok = False
|
||||
print(f"check_no_heap: {len(files)} C++ files, {total} baselined or new violation(s)")
|
||||
return ok
|
||||
|
||||
|
||||
def self_test():
|
||||
@@ -144,11 +175,15 @@ def self_test():
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
parser.add_argument("--root", type=pathlib.Path, default=pathlib.Path(__file__).resolve().parent.parent)
|
||||
parser.add_argument("--baseline", type=pathlib.Path,
|
||||
help="ratchet file (default: scripts/no_heap_baseline.txt under --root)")
|
||||
parser.add_argument("--self-test", action="store_true", help="run the pattern self-test first")
|
||||
args = parser.parse_args()
|
||||
if args.self_test and not self_test():
|
||||
return 1
|
||||
return 0 if check_tree(args.root.resolve()) else 1
|
||||
root = args.root.resolve()
|
||||
baseline = read_baseline(args.baseline or root / "scripts" / "no_heap_baseline.txt")
|
||||
return 0 if check_tree(root, baseline) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Heap-allocation ratchet for C++ sources converted mechanically from C.
|
||||
# Format: <path> <violations>. check_no_heap.py fails if a file gains
|
||||
# violations, and also when it loses some, so the number here always goes
|
||||
# down. Remove a file once it is rewritten on the src/core primitives.
|
||||
src/cache.cpp 3
|
||||
src/ipset.cpp 3
|
||||
src/json.cpp 2
|
||||
src/process.cpp 26
|
||||
src/rule.cpp 2
|
||||
src/ssurl.cpp 3
|
||||
Reference in New Issue
Block a user