diff --git a/.github/workflows/chapter-structure.yml b/.github/workflows/chapter-structure.yml new file mode 100644 index 0000000..f2afa8e --- /dev/null +++ b/.github/workflows/chapter-structure.yml @@ -0,0 +1,41 @@ +name: Chapter Markdown Structure +on: + pull_request: + paths: + - '01_low_level/**/README.md' + - 'scripts/check_chapter_structure.py' + - '.github/workflows/chapter-structure.yml' + push: + branches: [master] + paths: + - '01_low_level/**/README.md' + - 'scripts/check_chapter_structure.py' + - '.github/workflows/chapter-structure.yml' + workflow_dispatch: +jobs: + chapter-contract: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Validate Book 01 chapter structure + shell: bash + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + base="${{ github.event.pull_request.base.sha }}" + elif [[ "${{ github.event_name }}" == "push" ]]; then + base="${{ github.event.before }}" + else + python scripts/check_chapter_structure.py + exit + fi + mapfile -t files < <(git diff --name-only --diff-filter=ACMR "$base" HEAD -- '01_low_level/*/README.md') + if (( ${#files[@]} )); then + python scripts/check_chapter_structure.py "${files[@]}" + else + echo "No changed chapter READMEs to validate" + fi diff --git a/01_low_level/12_c_cpp/README.md b/01_low_level/12_c_cpp/README.md index a03b8c1..e57e011 100644 --- a/01_low_level/12_c_cpp/README.md +++ b/01_low_level/12_c_cpp/README.md @@ -1,4 +1,4 @@ -# 12. C/C++ Compatibility +# 12. C and C++ Compatibility from C++98 to C++26 This chapter focuses on **principal language and toolchain differences** that matter when you: - build mixed C/C++ projects @@ -9,40 +9,40 @@ This chapter focuses on **principal language and toolchain differences** that ma Some files are **C-only** (`.c`) and some are **C++** (`.cpp`) -## Files +## 1. Files -1. `01_name_mangling_and_overload.cpp` +1. `01_name_mangling_and_overload` examples - C++ name mangling, why overloads work - how to inspect symbols with `nm` / `objdump` - why C cannot overload by signature -2. `02_extern_c_and_callbacks.cpp` +2. `02_extern_c_callbacks` examples - `extern "C"`: linkage and ABI - C-callable functions exported from C++ - callbacks/function pointers across the boundary - pitfalls: exceptions, C++ objects across ABI -3. `03_static_init_and_main.cpp` +3. `03_static_init_and_main` examples - C++ startup: global constructors run **before `main`** - C startup: no user constructors/destructors - init-order fiasco + safe patterns -4. `04_headers_and_types.cpp` +4. `04_headers_and_types` examples - `` vs `` mapping - `void*` conversions, `malloc` in C vs C++ - `bool` vs `_Bool`, `nullptr` vs `NULL` - `restrict` in C99 and portable C++ replacements -5. `05_c_things_not_in_cpp.c` +5. `05_c_things_not_in_cpp` examples - Valid C features that are ill-formed in standard C++: VLAs, compound literals, array designators, `restrict`, implicit `void*` conversion, C struct-tag differences. -6. `06_cpp_things_not_in_c.cpp` +6. `06_cpp_things_not_in_c` examples - C++ features that C doesn't have (RAII/classes/templates/etc.) - a correct "C ABI facade" pattern: opaque handles + create/destroy + error codes -## ABI safety checklist for mixed projects +## 2. ABI safety checklist for mixed projects - `extern "C"` changes **linkage / mangling**, not C++ semantics. - Never let C++ exceptions cross into C: catch inside and return error codes. @@ -51,6 +51,128 @@ Some files are **C-only** (`.c`) and some are **C++** (`.cpp`) --- +## 3. C and C++ are different languages sharing an ABI only by agreement + +The shared heritage of C and C++ makes mixed-language projects seem simpler than they are. Compiling the same header as C17 and C++20 can produce different types, different name lookup, different initialization, and different interpretations of an expression. The interoperability contract is therefore an explicit interface between two languages, not an assumption that C++ is a strict superset of C. + +## 4. C linkage affects language linkage, not everything the ABI requires + +Declaring a function `extern "C"` gives it C language linkage, including rules governing its name and function type. This does not guarantee identical calling conventions across all targets, compatible definitions of C and C++ structs, or a stable exception ABI. + +```cpp +extern "C" int codec_decode(const unsigned char* bytes, + std::size_t length) noexcept; +``` + +The corresponding C header must use declarations legal in C, such as `size_t` from ``. Avoid putting `noexcept` in a shared C declaration; use conditional macros guarded by `__cplusplus`. An exported function catches every C++ exception internally and reports errors through a C-compatible result. + +## 5. The same `struct` layout is a requirement to verify, not assume + +C and C++ often agree on the layout of simple record types under a matching target ABI. But a C++ class with virtual functions, nonstandard layout, inheritance, or implementation-specific packing is not a candidate for direct C interchange. + +```c +/* header visible to C and C++ */ +typedef struct device_packet { + unsigned int id; + unsigned char mode; +} device_packet; +``` + +Define the shared ABI in terms of exact available fundamental or fixed-width types, packing/alignment rules, and size/version fields. Check `sizeof`, `alignof`, and `offsetof` on every supported target. For long-lived disk/network interchange, serialize fields explicitly instead of copying raw struct bytes. + +## 6. Allocation and deallocation must be paired across the boundary + +C's `malloc/free` allocation model and C++'s `new/delete` model are not interchangeable, especially when constructors and destructors are involved. A library that hands a resource to C callers must export the matching destroy operation; the caller should not guess the allocator family. + +```c +typedef struct codec_handle codec_handle; +codec_handle* codec_create(void); +void codec_destroy(codec_handle*); +``` + +The C++ implementation can use RAII internally while maintaining an opaque externally visible handle. This also shields the API from changes in internal class layout and C++ standard library implementation. + +## 7. Function pointers and callbacks need a deliberate lifetime protocol + +A C callback typically consists of a function pointer plus a context pointer. A capturing C++ lambda cannot implicitly become a C function pointer; a noncapturing lambda converts to a pointer to a function with C++ language linkage, so it is not a portable match for a C-linkage callback type, even if common compilers accept it. When the callback contract requires C linkage, use a named `extern "C"` trampoline. + +```c +typedef void (*on_result)(void* context, int status); +void codec_register(codec_handle*, on_result, void* context); +``` + +Never allow a callback to access an already-destroyed context. Document whether callbacks can run concurrently, after unregister, or during destruction; those decisions are more important than the lexical spelling of the callback declaration. + +## 8. Exceptions, RTTI, and standard-library objects must stay inside the C++ side + +A C caller cannot portably catch a C++ exception or understand `std::string`, `std::vector`, `std::shared_ptr`, or a C++ polymorphic object's ABI. Catch exceptions at each C-exported entry point, record diagnostics, and return an integer error code plus an explicit output buffer or handle. + +This is not just conservatism: differing compiler versions, standard libraries, runtime settings, and exception models can disagree about object layout, allocation, and exception unwinding. A stable C facade permits the C++ implementation to evolve without requiring every consumer to share its toolchain. + +## 9. C99 VLAs and compound literals are not ordinary C++ syntax + +A locally declared C variable-length array has automatic storage duration; ISO C does not prescribe a stack allocation mechanism. Standard C++ does not provide C99 VLAs. Some GCC and Clang C++ modes accept them as extensions, which can accidentally turn portable-looking code into vendor-specific code. + +```c +/* C99, not standard C++ */ +void compute(int n) { + int scratch[n]; + use(scratch); +} +``` + +A C compound literal also differs from C++ temporary-expression rules, including lifetime. Prefer `std::vector` or a bounded owner for dynamic C++ arrays, and explicitly manage the lifetime of any temporary view created while adapting a C API. + +## 10. C99 `restrict` has no ISO C++ spelling + +C's `restrict` is a pointer-aliasing contract with specific block execution semantics. C++20/23/26 do not include a portable `restrict` keyword; compilers offer `__restrict` or `__restrict__` extensions with toolchain-specific meaning. + +Passing two overlapping buffers to an API that assumes non-aliasing can invalidate optimizer assumptions. Prefer documenting actual disjointness preconditions and encapsulating compiler extensions rather than simply copying a C `restrict` qualifier into a shared header. + +## 11. C and C++ disagree about implicit conversions and type names + +C traditionally permits implicit conversion from `void*` to object pointers, while C++ requires a cast. C also has a distinct `struct` tag namespace, and C++ supports class names differently. These differences affect macros, generic API shims, and the shape of headers compiled by both compilers. + +```c +/* C permits this, standard C++ rejects the implicit conversion */ +int* p = malloc(100 * sizeof *p); +``` + +C++ should normally avoid raw `malloc` for objects requiring construction. A shared header can expose C-compatible opaque pointers while implementing typed ownership in the C++ translation unit. + +## 12. Compound type qualifiers and atomics have different models + +C11 `_Atomic` and C++11 `std::atomic` are not universally interchangeable ABI types. Their spelling, type requirements, initialization, and layout are not guaranteed to coincide, even when the platform uses the same underlying instructions. + +Do not expose an atomic object directly through a supposedly portable C/C++ boundary. Instead export operations that synchronize internally, or agree on a platform-specific ABI and document toolchain assumptions. + +## 13. C23 and C++23 share familiar tokens without sharing the whole language + +C23 introduced or standardized several spellings familiar to C++ developers, but C23's language rules are not C++23 rules. In mixed build systems compile `.c` sources with a chosen C standard and `.cpp` with a chosen C++ standard; do not expect one set of flags or feature-test macros to cover both languages. + +For example, `nullptr` and boolean support have evolving C definitions and availability, while C++ treats them through its own language type rules. Cross-language headers must explicitly conditionally declare facilities rather than assuming spellings imply binary compatibility. + +## 14. C++20 modules do not make C headers into portable modules automatically + +A C header has textual macro and declaration semantics. Importing a C++20 header unit or including that header in the global module fragment changes build architecture but does not eliminate underlying ABI issues, macro dependencies, or the need to compile C code with a C compiler. + +A safe migration starts from a C-facing header that remains valid C, then wraps it behind C++ classes and module interfaces. The wrapper owns resource handles and translates errors, callbacks, and strings across the boundary. + +## 15. C++26 reflection is not a shared ABI protocol + +Static reflection can help generate code around C++ types, but its existence does not automatically define a stable representation for C consumers, nor does it make C++ classes safe C structs. Use generated bindings only alongside a versioned C schema, explicit ownership rules, and ABI tests on all intended compilers. + +--- + +## 16. Cross-language review checklist for production APIs + +A C ABI should define allocator ownership, error handling, callback lifetime, thread-safety, calling convention, alignment, scalar widths, and versioning. For a plugin interface, keep an explicit versioned function table or handle-based API rather than passing compiler-specific C++ virtual objects through a foreign implementation. + +When in doubt, test the same public header under both C17/C23 and C++20/C++23 and compare binary exports with `nm`, `readelf`, and `dumpbin`. Runtime integration tests must exercise error paths and teardown paths, not only successful calls. + +--- + + ## Rules worth keeping in working memory The controlling distinction for C/C++ interoperability is between the portable language contract and the behavior of a particular compilation environment. C++ gives programmers the ability to build close to system boundaries, but also requires those boundaries to be specified rather than guessed. @@ -188,7 +310,7 @@ No material change. Modern library vocabulary still belongs behind the facade. -### C++26: Forthcoming support +### C++26: Reflection does not define an ABI Future language/library changes cannot remove the need for an explicit ABI contract. diff --git a/scripts/check_chapter_structure.py b/scripts/check_chapter_structure.py new file mode 100644 index 0000000..a4ba286 --- /dev/null +++ b/scripts/check_chapter_structure.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""Validate Book 01 technical-literature chapter contracts (stdlib only).""" +from __future__ import annotations + +import argparse +from pathlib import Path +import re +import sys + +ENDING = ( + "Rules worth keeping in working memory", + "Diagnostics, useful compiler settings and extensions", + "Standards timeline", + "Migration note for Java / Python / C# developers", + "Further reading", +) +YEARS = ("C++98/03", "C++11", "C++14", "C++17", "C++20", "C++23", "C++26") +STUBS = re.compile(r"\b(?:TODO|TBD|FIXME|PLACEHOLDER|fill this in)\b|", re.I) +HEADING = re.compile(r"^(#{1,6})\s+(.+?)\s*$", re.M) +FENCE_OPEN = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$") +FENCE_CLOSE = re.compile(r"^ {0,3}(`{3,}|~{3,})[ \t]*$") +CHAPTER = re.compile(r"^(\d{2})_") + + +def outside_fences(body: str) -> tuple[str, bool]: + visible: list[str] = [] + opening: tuple[str, int] | None = None + for line in body.splitlines(keepends=True): + if opening is None: + match = FENCE_OPEN.match(line) + if match and not (match.group(1).startswith("`") and "`" in match.group(2)): + delimiter = match.group(1) + opening = (delimiter[0], len(delimiter)) + visible.append("\n" if line.endswith("\n") else "") + else: + visible.append(line) + continue + match = FENCE_CLOSE.match(line) + if (match and match.group(1)[0] == opening[0] + and len(match.group(1)) >= opening[1]): + opening = None + visible.append("\n" if line.endswith("\n") else "") + return "".join(visible), opening is not None + + +def validate(path: Path) -> list[str]: + body = path.read_text(encoding="utf-8") + visible_body, unbalanced_fences = outside_fences(body) + errors: list[str] = [] + chapter = CHAPTER.match(path.parent.name) + if not chapter: + return [f"{path}: not a numbered chapter directory"] + headings = [(len(m.group(1)), m.group(2)) for m in HEADING.finditer(visible_body)] + if not headings or headings[0][0] != 1 or not headings[0][1].startswith(chapter.group(1)): + errors.append("H1 must start with chapter number") + h2 = [h for level, h in headings if level == 2] + if h2[-len(ENDING):] != list(ENDING): + errors.append("final five H2 sections must match the chapter template in order") + for h in ENDING: + if h2.count(h) != 1: + errors.append(f"expected exactly one H2: {h}") + numeric = [int(m.group(1)) for h in h2 if (m := re.match(r"^(\d+)\.\s", h))] + if not numeric or numeric != list(range(1, len(numeric) + 1)): + errors.append(f"numbered H2 sections not consecutive: {numeric}") + found_stubs = STUBS.findall(body) + if found_stubs: + errors.append(f"contains stub markers: {found_stubs[:3]}") + if unbalanced_fences: + errors.append("unbalanced fenced code blocks") + timeline_start = visible_body.find("\n## Standards timeline") + migration_start = visible_body.find("\n## Migration note") + if timeline_start >= 0 and migration_start > timeline_start: + timeline = visible_body[timeline_start:migration_start] + missing = [std for std in YEARS if not re.search(r"^### " + re.escape(std) + r"\b", timeline, re.M)] + if missing: + errors.append(f"standards timeline missing: {', '.join(missing)}") + if len(body) < 7000: + errors.append("chapter is unexpectedly short for advanced technical literature") + for item in errors: + print(f"{path}: {item}", file=sys.stderr) + if not errors: + print(f"PASS {path} ({len(numeric)} numbered sections)") + return errors + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("paths", nargs="*", type=Path, + help="README.md paths; defaults to chapters 01–13") + args = ap.parse_args() + paths = args.paths or sorted(Path("01_low_level").glob("[0-9][0-9]_*/README.md")) + if not paths: + print("No chapter READMEs found", file=sys.stderr) + return 2 + failures = sum(bool(validate(p)) for p in paths) + return 1 if failures else 0 + +if __name__ == "__main__": + raise SystemExit(main())