Files
rocksdb/tools/c_api_gen/check_api_compatibility.py
T
zaidoon 1cec28d82d Finish C API code generation (continues #14572) (#14868)
Summary:
This continues and finishes **https://github.com/facebook/rocksdb/issues/14572** ("Add semi-automated code generation for RocksDB C API bindings") by xingbowang. The original author is unavailable to finish it, so I've taken it over. **All 13 of the original commits are preserved** (this branch was created from the PR head and builds on top of it — `git log` shows the original `Xingbo Wang` authorship intact); my follow-up work is in the commits prefixed `C API codegen:`.

The underlying design is unchanged and is the original author's: hand-written source templates (`tools/c_api_gen/c_base.h` / `c_base.cc`) plus two generators (auto-discovery from the C++ headers + a spec-driven generator) are inlined into a single, self-contained, `generated` `include/rocksdb/c.h` and `db/c.cc`. This grows the public C API by **668 functions** while keeping `c.h` a single includable header with no `-I` requirement (so `bindgen` and other FFI tools keep working unchanged).

This branch reconciles the PR with ~4 months of `main` and addresses the outstanding review feedback (clang-tidy bot, the automated code review, the `c.h` self-containedness discussion, and pdillinger's points about `include/rocksdb` hygiene and `generated` marking).

## What changed on top of the original PR

### Reconciled with current `main`
- Merged current `main` (conflicts were confined to the generated/test files) and regenerated. Reconciled the 14 C API functions `main` added since the merge-base (e.g. `rocksdb_set_db_options`, the backup-engine rate limiters, `memtable_batch_lookup_optimization`, `optimize_multiget_for_io`, …) and restored 5 enum constants that upstream had added by hand (`rocksdb_txndb_write_policy_*`, `..._index_block_search_type_auto`, `rocksdb_blob_cache_read_byte`).

### Maintainer feedback (pdillinger)
- **No non-user-includable files in `include/rocksdb`.** Moved the hand-written templates out of `include/rocksdb/` and `db/` to `tools/c_api_gen/c_base.{h,cc}`. They were `#include`-ing generated fragments, which broke `make check-headers` and was shipped by `make install`. `include/rocksdb/` now contains only the user-facing, self-contained, `generated` `c.h`.
- `c.h` / `c.cc` carry the `// generated` marker.

### Backward compatibility (zero ABI break)
- The generator derived each wrapper's C type purely from the C++ field, which had silently changed **5 already-shipped signatures** (e.g. `rocksdb_writeoptions_disable_WAL` `int` → `unsigned char`). Added an ABI type-pinning layer (`tools/c_api_gen/abi_type_overrides.json`) so already-shipped functions keep their historical C signature (the body still casts to the real field type). A repo-wide diff against the merge-base now reports **0 ABI drift**.
- New `check_api_compatibility.py` gate (wired into CI + `make`) fails on any removed/changed public function **or** removed enum/typedef symbol, vs a reference revision. Intentional changes go in an allowlist with a reason.

### Correctness (from the automated review)
- Restored 5 option setters that were declared in `c.h` but **defined nowhere** (link failure for downstream bindings such as `rust-rocksdb`). Added `check_api_completeness.py` (dependency-free; runs in CI + `make`) asserting every declared function has exactly one definition — this is the gate that would have caught it.
- `CopyStringVector` now null-checks `malloc`; the WAL filter `std::move`s the `WriteBatch`; the backup exclude-files callback captures by value instead of the wrapper pointer.

### Build / CI robustness
- Removed the dead `C_API_CODEGEN_STAMP` Makefile prerequisite (it was a silent no-op).
- The `make check` staleness check is now opt-out-able (behind `SKIP_FORMAT_BUCK_CHECKS`) and skips gracefully when `clang++` is unavailable, so `make check` works without the codegen toolchain. CI remains the authoritative gate.
- Pinned `clang-format` consistently through `regen_all.py` / `verify_generated_up_to_date.py` (CI uses clang-format-21) so regeneration is byte-reproducible across environments.
- Cleared all 20 `clang-tidy` warnings the bot reported on `db/c.cc` changed lines (fixed in the `c_base.cc` template, not the generated output).
- Updated the internal Buck `c_test_bin` wrapper to expose generated `c_api_gen/*.inc` fragments as headers, so sandboxed Buck builds can compile `db/c_test.c` after the generated round-trip tests are included.

### Test coverage
- Added `gen_roundtrip_tests.py`, which derives **462 set→get→assert round-trip checks across 25 option objects** from the same generated fragments and wires them into `db/c_test.c`. Coverage now tracks the generated surface automatically.

### Docs
- Added the `unreleased_history/public_api_changes` note and fixed `claude_md/add_public_api.md`, which still told contributors to hand-edit the now-`generated` `c.h`/`c.cc`.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14868

Test Plan:
- `make c_test && ./c_test` — **passes**, including the 462 generated round-trip assertions (a successful link also confirms the API is complete).
- `python3 tools/c_api_gen/check_api_completeness.py` — all 1737 declared functions defined exactly once.
- `python3 tools/c_api_gen/check_api_compatibility.py --ref <release>` — 1070 reference functions + 229 enum/typedef symbols preserved, 0 removed/changed.
- `python3 tools/c_api_gen/verify_generated_up_to_date.py` — generated output is stable.
- `include/rocksdb/c.h` confirmed self-contained (only `<stdbool.h>`, `<stddef.h>`, `<stdint.h>`).

cc xingbowang

- `buck2 build --flagfile fbcode//mode/dev fbcode//internal_repo_rocksdb/repo:c_test_bin` — passes.
- `buck2 build --flagfile fbcode//mode/dev --config fbcode.arch=aarch64 fbcode//internal_repo_rocksdb/repo:c_test_bin` — passes.

Reviewed By: pdillinger

Differential Revision: D109149150

Pulled By: xingbowang

fbshipit-source-id: 3417375345f360a4c78bdfe27e9850b89d0a226a
2026-06-24 10:45:42 -07:00

238 lines
7.9 KiB
Python

# Copyright (c) Meta Platforms, Inc. and affiliates.
# This source code is licensed under both the GPLv2 (found in the COPYING file in the root directory)
# and the Apache 2.0 License (found in the Apache file in the root directory).
#!/usr/bin/env python3
"""Check the public C API for backward-incompatible changes vs a reference.
RocksDB's C API (include/rocksdb/c.h) has a strong backward-compatibility
guarantee. Because most wrappers are now code-generated from the C++ headers, a
careless C++ change (renamed/removed field, changed field type) or a generator
change can silently remove a public C function or change its signature -- an
ABI/API break for every downstream language binding.
This is a robust, signature-level gate (it compares declared function
signatures, not function bodies, so it has no false positives from formatting
or body-token differences). For every function present in the reference
revision's c.h it requires that the current c.h still declares it with an
identical signature (parameter names are ignored; they do not affect the C
ABI). New functions are reported but never fail the check.
Intentional, reviewed incompatibilities (e.g. removing a function that was
declared but never implemented) must be recorded in
api_compatibility_allowlist.json with a reason.
Usage:
python3 tools/c_api_gen/check_api_compatibility.py [--ref main]
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
C_HEADER_REL = "include/rocksdb/c.h"
C_HEADER = ROOT / C_HEADER_REL
ALLOWLIST_PATH = ROOT / "tools/c_api_gen/api_compatibility_allowlist.json"
# C type-name tokens that are never a parameter *name*. Used to tell
# "unsigned char" (type, no name) apart from "rocksdb_t db" (type + name).
_TYPE_KEYWORDS = frozenset(
{
"void",
"char",
"short",
"int",
"long",
"float",
"double",
"unsigned",
"signed",
"bool",
"const",
"size_t",
"ssize_t",
"intptr_t",
"uintptr_t",
"ptrdiff_t",
}
)
DECL_RE = re.compile(
r"extern\s+ROCKSDB_LIBRARY_API\s+([\s\S]*?)\b(rocksdb_[A-Za-z0-9_]+)\s*\("
r"([^;]*?)\)\s*;",
)
def _strip_param_name(param: str) -> str:
"""Return the parameter type with any trailing parameter name removed."""
param = " ".join(param.replace("[]", " []").split())
if not param:
return param
tokens = param.split()
last = tokens[-1]
if (
last in _TYPE_KEYWORDS
or last.endswith("_t")
or "*" in last
or last == "[]"
):
return param # unnamed parameter; nothing to strip
if len(tokens) > 1 and re.fullmatch(r"[A-Za-z_]\w*", last):
return " ".join(tokens[:-1])
return param
def parse_signatures(header_text: str) -> dict[str, str]:
"""Map function name -> normalized signature string (param names removed)."""
sigs: dict[str, str] = {}
for match in DECL_RE.finditer(header_text):
ret = " ".join(match.group(1).split())
name = match.group(2)
params = [
_strip_param_name(p) for p in match.group(3).split(",") if p.strip()
]
sigs[name] = f"{ret} {name}({', '.join(params)})"
return sigs
def parse_symbols(header_text: str) -> set[str]:
"""Non-function public identifiers: enum constants and typedef names.
These are part of the C API contract too (e.g. rocksdb_txndb_write_policy_*,
the rocksdb tickers enum). A rocksdb_* token followed by '=', ',' or '}' is
an enum constant; one introduced/closed by typedef is a type name. Anything
followed by '(' is a function (handled by parse_signatures) and excluded.
"""
functions = {m.group(2) for m in DECL_RE.finditer(header_text)}
functions |= set(re.findall(r"\b(rocksdb_[A-Za-z0-9_]+)\s*\(", header_text))
symbols: set[str] = set()
for m in re.finditer(r"\b(rocksdb_[A-Za-z0-9_]+)\s*(?:=|,|\}|;)", header_text):
symbols.add(m.group(1))
symbols |= set(
re.findall(r"typedef\s+struct\s+(rocksdb_[A-Za-z0-9_]+)", header_text)
)
return symbols - functions
def git_show(ref: str, path: str) -> str | None:
proc = subprocess.run(
["git", "show", f"{ref}:{path}"],
cwd=ROOT,
text=True,
capture_output=True,
check=False,
)
if proc.returncode != 0:
return None
return proc.stdout
def load_allowlist() -> dict[str, set[str]]:
"""Allowed incompatibilities keyed by kind: removed / changed / removed_symbol."""
allowed = {"removed": set(), "changed": set(), "removed_symbol": set()}
if not ALLOWLIST_PATH.exists():
return allowed
data = json.loads(ALLOWLIST_PATH.read_text())
for entry in data.get("allow", []):
kind = entry.get("kind")
name = entry.get("function") or entry.get("symbol")
if kind in allowed and name:
allowed[kind].add(name)
return allowed
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--ref",
default="main",
help=(
"git revision to treat as the compatibility baseline (default: "
"main). The current working-tree c.h must stay compatible with it."
),
)
args = parser.parse_args()
ref_text = git_show(args.ref, C_HEADER_REL)
if ref_text is None:
sys.stderr.write(
f"error: could not read {C_HEADER_REL} at revision '{args.ref}'. "
"Pass an existing revision via --ref (e.g. --ref origin/main).\n"
)
return 2
cur_text = C_HEADER.read_text()
ref = parse_signatures(ref_text)
cur = parse_signatures(cur_text)
allowed = load_allowlist()
removed = sorted(n for n in ref if n not in cur and n not in allowed["removed"])
changed = sorted(
n
for n in ref
if n in cur and ref[n] != cur[n] and n not in allowed["changed"]
)
new = sorted(n for n in cur if n not in ref)
ref_symbols = parse_symbols(ref_text)
cur_symbols = parse_symbols(cur_text)
removed_symbols = sorted(
s
for s in ref_symbols
if s not in cur_symbols and s not in allowed["removed_symbol"]
)
status = 0
if removed:
status = 1
sys.stderr.write(
f"error: {len(removed)} public C API function(s) present at "
f"'{args.ref}' are missing now (backward-incompatible removal):\n"
)
for name in removed:
sys.stderr.write(f" - {name}\n")
if changed:
status = 1
sys.stderr.write(
f"error: {len(changed)} public C API function(s) changed signature "
f"(backward-incompatible ABI change):\n"
)
for name in changed:
sys.stderr.write(f" - {name}\n")
sys.stderr.write(f" was: {ref[name]}\n")
sys.stderr.write(f" now: {cur[name]}\n")
if removed_symbols:
status = 1
sys.stderr.write(
f"error: {len(removed_symbols)} public C API symbol(s) (enum "
f"constants / typedefs) present at '{args.ref}' are missing now "
"(backward-incompatible removal):\n"
)
for name in removed_symbols:
sys.stderr.write(f" - {name}\n")
if status != 0:
sys.stderr.write(
"\nIf a change is intentional and reviewed, record it in\n"
f"{ALLOWLIST_PATH.relative_to(ROOT)} with a reason; otherwise restore "
"the original declaration (see tools/c_api_gen/abi_type_overrides.json "
"for pinning a generated C type).\n"
)
else:
print(
f"C API is backward-compatible with '{args.ref}': "
f"{len(ref)} reference functions preserved, {len(new)} new; "
f"{len(ref_symbols)} enum/typedef symbols preserved."
)
return status
if __name__ == "__main__":
raise SystemExit(main())