Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/chapter-structure.yml
Original file line number Diff line number Diff line change
@@ -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
142 changes: 132 additions & 10 deletions 01_low_level/12_c_cpp/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
- `<string.h>` vs `<cstring>` 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.
Expand All @@ -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 `<stddef.h>`. 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<T>` 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.
Expand Down Expand Up @@ -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.

Expand Down
98 changes: 98 additions & 0 deletions scripts/check_chapter_structure.py
Original file line number Diff line number Diff line change
@@ -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|<placeholder>", 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())
Loading