mirror of
https://github.com/facebook/rocksdb.git
synced 2026-07-07 14:47:40 +08:00
1cec28d82d
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
167 lines
5.1 KiB
Python
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())
|