Files
rocksdb/tools/c_api_gen/verify_generated_up_to_date.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

167 lines
5.1 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 LICENSE.Apache file in the root directory).
#!/usr/bin/env python3
"""Verify checked-in C API generated fragments are up to date without Git."""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
REGEN_ALL = ROOT / "tools/c_api_gen/regen_all.py"
CLANG_FORMAT_STYLE_FILE = ROOT / ".clang-format"
GENERATED_DIRS = (
Path("c_api_gen"),
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Regenerate the checked-in C API fragments and compare them against "
"a pre-regen snapshot without relying on git diff."
)
)
parser.add_argument(
"--clang-format",
default="clang-format",
help=(
"clang-format binary to canonicalize both the pre-regen snapshot "
"and regenerated output before comparison (default: clang-format)"
),
)
return parser.parse_args()
def resolve_executable(command: str) -> str:
if Path(command).parent != Path():
if Path(command).exists():
return str(Path(command))
raise FileNotFoundError(f"Executable not found: {command}")
resolved = shutil.which(command)
if resolved is None:
raise FileNotFoundError(
f"{command} not found in PATH. Install it or pass --clang-format."
)
return resolved
# c.h and c.cc are also generated artifacts inlined from c_base.* + .inc fragments
GENERATED_FILES = (
Path("include/rocksdb/c.h"),
Path("db/c.cc"),
)
def copy_generated_dirs(destination_root: Path) -> None:
for rel_dir in GENERATED_DIRS:
source = ROOT / rel_dir
if not source.is_dir():
raise FileNotFoundError(f"Generated directory missing: {source}")
destination = destination_root / rel_dir
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(source, destination)
for rel_file in GENERATED_FILES:
source = ROOT / rel_file
if not source.is_file():
raise FileNotFoundError(f"Generated file missing: {source}")
destination = destination_root / rel_file
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination)
def collect_inc_files(root: Path) -> list[str]:
inc_files: list[str] = []
for rel_dir in GENERATED_DIRS:
directory = root / rel_dir
if not directory.is_dir():
raise FileNotFoundError(f"Generated directory missing: {directory}")
inc_files.extend(str(path) for path in sorted(directory.rglob("*.inc")))
return inc_files
def format_generated_dirs(root: Path, clang_format: str) -> None:
inc_files = collect_inc_files(root)
if not inc_files:
raise RuntimeError(f"No generated .inc files found under {root}")
subprocess.run(
[
clang_format,
f"--style=file:{CLANG_FORMAT_STYLE_FILE}",
"-i",
*inc_files,
],
cwd=root,
check=True,
)
def compare_snapshots(work_root: Path) -> int:
proc = subprocess.run(
["diff", "-ruN", "checked_in", "regenerated"],
cwd=work_root,
text=True,
capture_output=True,
check=False,
)
if proc.returncode == 0:
print("C API generated files are up to date.")
return 0
if proc.returncode == 1:
sys.stderr.write(
"C API generated files are out of date. Re-run "
"`python3 tools/c_api_gen/regen_all.py` and commit the updated "
"files under c_api_gen/, "
"include/rocksdb/c.h, and db/c.cc.\n"
)
if proc.stdout:
sys.stderr.write(proc.stdout)
return 1
if proc.stdout:
sys.stderr.write(proc.stdout)
if proc.stderr:
sys.stderr.write(proc.stderr)
raise RuntimeError(f"diff failed with exit code {proc.returncode}")
def main() -> int:
args = parse_args()
clang_format = resolve_executable(args.clang_format)
with tempfile.TemporaryDirectory(prefix="rocksdb-c-api-gen-verify-") as tmp:
work_root = Path(tmp)
checked_in_root = work_root / "checked_in"
regenerated_root = work_root / "regenerated"
copy_generated_dirs(checked_in_root)
format_generated_dirs(checked_in_root, clang_format)
# Pass the pinned formatter down so the regenerated .inc fragments (and
# thus the inlined c.h/c.cc) are canonicalized with the same clang-format
# version on both sides of the comparison.
subprocess.run(
[sys.executable, str(REGEN_ALL), "--clang-format", args.clang_format],
cwd=ROOT,
check=True,
)
format_generated_dirs(ROOT, clang_format)
copy_generated_dirs(regenerated_root)
return compare_snapshots(work_root)
if __name__ == "__main__":
raise SystemExit(main())