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

1305 lines
45 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
"""Auto-discover and generate simple RocksDB C API field bindings.
This script follows a strict policy for the families it manages:
- if a supported simple public C++ field already has matching C bindings,
leave the existing binding in place
- otherwise auto-generate the matching C binding
- if a managed field is explicitly blocklisted as manual or deferred,
leave it for explicit C API follow-up
- if a field is in a managed family but is not supported by the simple
generator and there is no manual C binding, fail regeneration
"""
from __future__ import annotations
import argparse
import json
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass
from json import JSONDecoder
from pathlib import Path
from typing import Iterable
from generate_c_api import render_decl, render_impl, write_if_changed
ROOT = Path(__file__).resolve().parents[2]
INCLUDE_ROOT = ROOT / "include/rocksdb"
SOURCE_ROOT = ROOT / "db"
# Hand-written source templates for the C API. These are generator inputs (not
# user-includable / not compiled), so they live next to the generator rather
# than in include/rocksdb or db/.
TEMPLATE_ROOT = ROOT / "tools/c_api_gen"
GENERATED_INCLUDE_ROOT = ROOT / "c_api_gen"
GENERATED_SOURCE_ROOT = ROOT / "c_api_gen"
CLANG_FORMAT = shutil.which("clang-format")
BLOCKLIST_PATH = ROOT / "tools/c_api_gen/auto_simple_bindings_blocklist.json"
BLOCKLIST_POLICIES = frozenset({"manual", "deferred"})
# ABI-preserving C type overrides: function name -> pinned C type. See
# abi_type_overrides.json for the rationale (RocksDB's C API must not change the
# signature of an already-shipped function even when the C++ field type maps to
# a different C type).
ABI_TYPE_OVERRIDES_PATH = ROOT / "tools/c_api_gen/abi_type_overrides.json"
def load_abi_type_overrides(path: Path) -> dict[str, str]:
import json
if not path.exists():
return {}
data = json.loads(path.read_text())
overrides: dict[str, str] = {}
for entry in data.get("overrides", []):
name = entry["function"]
if name in overrides and overrides[name] != entry["c_type"]:
raise ValueError(
f"Conflicting ABI type overrides for {name}: "
f"{overrides[name]} vs {entry['c_type']}"
)
overrides[name] = entry["c_type"]
return overrides
ABI_TYPE_OVERRIDES = load_abi_type_overrides(ABI_TYPE_OVERRIDES_PATH)
@dataclass(frozen=True)
class FamilyConfig:
struct_name: str
header: Path
c_receiver_type: str
c_receiver_name: str
c_prefix: str
mode: str
field_expr_format: str = "{receiver_name}->rep.{field_name}"
enum_types: tuple[str, ...] = ()
# C integer type used for enum-typed fields. This is intentionally
# per-family to preserve each family's historical ABI: the option families
# have always exposed enums as `int`, while the listener/job-info families
# shipped them as `uint32_t` (e.g. the pre-existing
# rocksdb_flushjobinfo_flush_reason). Do NOT "unify" these to one type --
# it would change the signatures of already-shipped functions.
enum_c_type: str = "int"
section_name: str | None = None
setter_name_overrides: tuple[tuple[str, str], ...] = ()
getter_name_overrides: tuple[tuple[str, str], ...] = ()
@dataclass(frozen=True)
class OutputGroup:
name: str
header_out: Path
source_out: Path
families: tuple[FamilyConfig, ...]
@dataclass(frozen=True)
class BlocklistEntry:
family_name: str
field_name: str
policy: str
reason: str
tracking_issue: str | None = None
READ_OPTIONS_GROUP = OutputGroup(
name="Auto-discovered ReadOptions simple",
header_out=GENERATED_INCLUDE_ROOT / "c_generated_readoptions_auto.h.inc",
source_out=GENERATED_SOURCE_ROOT / "c_generated_readoptions_auto.cc.inc",
families=(
FamilyConfig(
struct_name="ReadOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_readoptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_readoptions",
mode="options",
enum_types=("ReadTier", "Env::IOPriority", "Env::IOActivity"),
enum_c_type="int",
),
),
)
OPTION_STRUCTS_GROUP = OutputGroup(
name="Auto-discovered option structs simple",
header_out=GENERATED_INCLUDE_ROOT / "c_generated_option_structs_auto.h.inc",
source_out=GENERATED_SOURCE_ROOT / "c_generated_option_structs_auto.cc.inc",
families=(
FamilyConfig(
struct_name="DBOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_options",
mode="options",
enum_types=(
"InfoLogLevel",
"WALRecoveryMode",
"CompressionType",
"CacheTier",
"Temperature",
),
setter_name_overrides=(
("compaction_readahead_size", "rocksdb_options_compaction_readahead_size"),
),
),
FamilyConfig(
struct_name="AdvancedColumnFamilyOptions",
header=INCLUDE_ROOT / "advanced_options.h",
c_receiver_type="rocksdb_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_options",
mode="options",
enum_types=(
"CompressionType",
"CompactionStyle",
"CompactionPri",
"Temperature",
"PrepopulateBlobCache",
"VerifyOutputFlags",
),
setter_name_overrides=(
(
"enable_blob_garbage_collection",
"rocksdb_options_set_enable_blob_gc",
),
(
"blob_garbage_collection_age_cutoff",
"rocksdb_options_set_blob_gc_age_cutoff",
),
(
"blob_garbage_collection_force_threshold",
"rocksdb_options_set_blob_gc_force_threshold",
),
),
getter_name_overrides=(
(
"enable_blob_garbage_collection",
"rocksdb_options_get_enable_blob_gc",
),
(
"blob_garbage_collection_age_cutoff",
"rocksdb_options_get_blob_gc_age_cutoff",
),
(
"blob_garbage_collection_force_threshold",
"rocksdb_options_get_blob_gc_force_threshold",
),
),
),
FamilyConfig(
struct_name="ColumnFamilyOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_options",
mode="options",
enum_types=("CompressionType",),
),
FamilyConfig(
struct_name="WriteOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_writeoptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_writeoptions",
mode="options",
enum_types=("Env::IOPriority", "Env::IOActivity"),
setter_name_overrides=(
("disableWAL", "rocksdb_writeoptions_disable_WAL"),
),
),
FamilyConfig(
struct_name="FlushOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_flushoptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_flushoptions",
mode="options",
),
FamilyConfig(
struct_name="FlushWALOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_flushwaloptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_flushwaloptions",
mode="options",
enum_types=("Env::IOPriority",),
),
FamilyConfig(
struct_name="EnvOptions",
header=INCLUDE_ROOT / "env.h",
c_receiver_type="rocksdb_envoptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_envoptions",
mode="options",
setter_name_overrides=(
("set_fd_cloexec", "rocksdb_envoptions_set_fd_cloexec"),
),
getter_name_overrides=(
("set_fd_cloexec", "rocksdb_envoptions_get_fd_cloexec"),
),
),
FamilyConfig(
struct_name="TraceOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_trace_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_trace_options",
mode="options",
),
FamilyConfig(
struct_name="BlockCacheTraceOptions",
header=INCLUDE_ROOT / "block_cache_trace_writer.h",
c_receiver_type="rocksdb_block_cache_trace_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_block_cache_trace_options",
mode="options",
),
FamilyConfig(
struct_name="BlockCacheTraceWriterOptions",
header=INCLUDE_ROOT / "block_cache_trace_writer.h",
c_receiver_type="rocksdb_block_cache_trace_writer_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_block_cache_trace_writer_options",
mode="options",
),
FamilyConfig(
struct_name="CompactionOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_compaction_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_compaction_options",
mode="options",
enum_types=("CompressionType", "Temperature"),
),
FamilyConfig(
struct_name="CompactRangeOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_compactoptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_compactoptions",
mode="options",
enum_types=("BottommostLevelCompaction", "BlobGarbageCollectionPolicy"),
enum_c_type="int",
),
FamilyConfig(
struct_name="IngestExternalFileOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_ingestexternalfileoptions_t*",
c_receiver_name="opt",
c_prefix="rocksdb_ingestexternalfileoptions",
mode="options",
),
FamilyConfig(
struct_name="OpenAndCompactOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_open_and_compact_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_open_and_compact_options",
mode="options",
),
FamilyConfig(
struct_name="ImportColumnFamilyOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_import_column_family_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_import_column_family_options",
mode="options",
),
FamilyConfig(
struct_name="SizeApproximationOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_size_approximation_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_size_approximation_options",
mode="options",
),
FamilyConfig(
struct_name="LiveFilesStorageInfoOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_livefiles_storage_info_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_livefiles_storage_info_options",
mode="options",
),
FamilyConfig(
struct_name="GetColumnFamilyMetaDataOptions",
header=INCLUDE_ROOT / "metadata.h",
c_receiver_type="rocksdb_column_family_metadata_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_column_family_metadata_options",
mode="options",
),
FamilyConfig(
struct_name="WaitForCompactOptions",
header=INCLUDE_ROOT / "options.h",
c_receiver_type="rocksdb_wait_for_compact_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_wait_for_compact_options",
mode="options",
),
FamilyConfig(
struct_name="BlockBasedTableOptions",
header=INCLUDE_ROOT / "table.h",
c_receiver_type="rocksdb_block_based_table_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_block_based_options",
mode="options",
enum_types=(
"IndexType",
"BlockSearchType",
"DataBlockIndexType",
"ChecksumType",
"IndexShorteningMode",
"PrepopulateBlockCache",
),
),
FamilyConfig(
struct_name="CuckooTableOptions",
header=INCLUDE_ROOT / "table.h",
c_receiver_type="rocksdb_cuckoo_table_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_cuckoo_options",
mode="options",
),
FamilyConfig(
struct_name="CompactionOptionsUniversal",
header=INCLUDE_ROOT / "universal_compaction.h",
c_receiver_type="rocksdb_universal_compaction_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_universal_compaction_options",
mode="options",
field_expr_format="{receiver_name}->rep->{field_name}",
enum_types=("CompactionStopStyle",),
),
FamilyConfig(
struct_name="CompactionOptionsFIFO",
header=INCLUDE_ROOT / "advanced_options.h",
c_receiver_type="rocksdb_fifo_compaction_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_fifo_compaction_options",
mode="options",
),
FamilyConfig(
struct_name="BackupEngineOptions",
header=INCLUDE_ROOT / "utilities/backup_engine.h",
c_receiver_type="rocksdb_backup_engine_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_backup_engine_options",
mode="options",
enum_types=("ShareFilesNaming",),
),
FamilyConfig(
struct_name="CreateBackupOptions",
header=INCLUDE_ROOT / "utilities/backup_engine.h",
c_receiver_type="rocksdb_create_backup_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_create_backup_options",
mode="options",
enum_types=("CpuPriority",),
),
FamilyConfig(
struct_name="RestoreOptions",
header=INCLUDE_ROOT / "utilities/backup_engine.h",
c_receiver_type="rocksdb_restore_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_restore_options",
mode="options",
enum_types=("Mode",),
),
FamilyConfig(
struct_name="TransactionDBOptions",
header=INCLUDE_ROOT / "utilities/transaction_db.h",
c_receiver_type="rocksdb_transactiondb_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_transactiondb_options",
mode="options",
enum_types=("TxnDBWritePolicy",),
),
FamilyConfig(
struct_name="TransactionOptions",
header=INCLUDE_ROOT / "utilities/transaction_db.h",
c_receiver_type="rocksdb_transaction_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_transaction_options",
mode="options",
),
FamilyConfig(
struct_name="OptimisticTransactionOptions",
header=INCLUDE_ROOT / "utilities/optimistic_transaction_db.h",
c_receiver_type="rocksdb_optimistictransaction_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_optimistictransaction_options",
mode="options",
),
FamilyConfig(
struct_name="OptimisticTransactionDBOptions",
header=INCLUDE_ROOT / "utilities/optimistic_transaction_db.h",
c_receiver_type="rocksdb_optimistictransactiondb_options_t*",
c_receiver_name="opt",
c_prefix="rocksdb_optimistictransactiondb_options",
mode="options",
enum_types=("OccValidationPolicy",),
),
),
)
JOBINFO_GROUP = OutputGroup(
name="Auto-discovered JobInfo metadata simple",
header_out=GENERATED_INCLUDE_ROOT / "c_generated_jobinfo_auto.h.inc",
source_out=GENERATED_SOURCE_ROOT / "c_generated_jobinfo_auto.cc.inc",
families=(
FamilyConfig(
struct_name="FlushJobInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_flushjobinfo_t*",
c_receiver_name="info",
c_prefix="rocksdb_flushjobinfo",
mode="metadata",
enum_types=("FlushReason", "CompressionType"),
enum_c_type="uint32_t",
),
FamilyConfig(
struct_name="CompactionJobInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_compactionjobinfo_t*",
c_receiver_name="info",
c_prefix="rocksdb_compactionjobinfo",
mode="metadata",
enum_types=("CompactionReason", "CompressionType"),
enum_c_type="uint32_t",
),
FamilyConfig(
struct_name="SubcompactionJobInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_subcompactionjobinfo_t*",
c_receiver_name="info",
c_prefix="rocksdb_subcompactionjobinfo",
mode="metadata",
enum_types=("CompactionReason", "CompressionType"),
enum_c_type="uint32_t",
),
FamilyConfig(
struct_name="ExternalFileIngestionInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_externalfileingestioninfo_t*",
c_receiver_name="info",
c_prefix="rocksdb_externalfileingestioninfo",
mode="metadata",
),
FamilyConfig(
struct_name="MemTableInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_memtableinfo_t*",
c_receiver_name="info",
c_prefix="rocksdb_memtableinfo",
mode="metadata",
),
),
)
METADATA_VIEW_GROUP = OutputGroup(
name="Auto-discovered metadata view structs simple",
header_out=GENERATED_INCLUDE_ROOT / "c_generated_metadata_structs_auto.h.inc",
source_out=GENERATED_SOURCE_ROOT / "c_generated_metadata_structs_auto.cc.inc",
families=(
FamilyConfig(
struct_name="TableProperties",
header=INCLUDE_ROOT / "table_properties.h",
c_receiver_type="const rocksdb_table_properties_t*",
c_receiver_name="props",
c_prefix="rocksdb_table_properties",
mode="metadata",
),
FamilyConfig(
struct_name="CompactionJobStats",
header=INCLUDE_ROOT / "compaction_job_stats.h",
c_receiver_type="const rocksdb_compaction_job_stats_t*",
c_receiver_name="stats",
c_prefix="rocksdb_compaction_job_stats",
mode="metadata",
),
FamilyConfig(
struct_name="CompactionFileInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_compaction_file_info_t*",
c_receiver_name="info",
c_prefix="rocksdb_compaction_file_info",
mode="metadata",
),
FamilyConfig(
struct_name="BlobFileAdditionInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_blob_file_addition_info_t*",
c_receiver_name="info",
c_prefix="rocksdb_blob_file_addition_info",
mode="metadata",
),
FamilyConfig(
struct_name="BlobFileGarbageInfo",
header=INCLUDE_ROOT / "listener.h",
c_receiver_type="const rocksdb_blob_file_garbage_info_t*",
c_receiver_name="info",
c_prefix="rocksdb_blob_file_garbage_info",
mode="metadata",
),
),
)
OUTPUT_GROUPS = (
READ_OPTIONS_GROUP,
OPTION_STRUCTS_GROUP,
JOBINFO_GROUP,
METADATA_VIEW_GROUP,
)
MANAGED_FAMILY_NAMES = frozenset(
family.struct_name for group in OUTPUT_GROUPS for family in group.families
)
AUTO_OUTPUTS = {
group.header_out.resolve() for group in OUTPUT_GROUPS
} | {group.source_out.resolve() for group in OUTPUT_GROUPS}
FUNCTION_RE = re.compile(r"\brocksdb_[A-Za-z0-9_]+\b(?=\s*\()")
HEADER_INCLUDE_RE = re.compile(r'^#include "(c_api_gen/[^"]+\.inc)"$', re.MULTILINE)
SOURCE_INCLUDE_RE = re.compile(r'^#include "(c_api_gen/[^"]+\.inc)"$', re.MULTILINE)
TOKEN_RE = re.compile(r"[A-Z]+(?=[A-Z][a-z0-9]|_|$)|[A-Z]?[a-z0-9]+|[A-Z]+")
def display_path(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except ValueError:
return str(path)
def _require_non_empty_string(value: object, context: str, errors: list[str]) -> str | None:
if not isinstance(value, str):
errors.append(f"{context} must be a non-empty string")
return None
stripped = value.strip()
if not stripped:
errors.append(f"{context} must be a non-empty string")
return None
return stripped
def load_blocklist(path: Path) -> tuple[dict[str, dict[str, BlocklistEntry]], list[str]]:
rel_path = display_path(path)
try:
raw = json.loads(path.read_text())
except FileNotFoundError:
return {}, [f"blocklist file not found: {rel_path}"]
except json.JSONDecodeError as exc:
return {}, [
f"invalid blocklist JSON: {rel_path}:{exc.lineno}:{exc.colno}: {exc.msg}"
]
errors: list[str] = []
blocklist: dict[str, dict[str, BlocklistEntry]] = {}
if not isinstance(raw, dict):
return {}, [f"blocklist root must be an object: {rel_path}"]
schema_version = raw.get("schema_version")
if schema_version != 1:
errors.append(
f"unsupported blocklist schema_version in {rel_path}: {schema_version!r}"
)
families = raw.get("families")
if not isinstance(families, list):
errors.append(f"{rel_path}: `families` must be a list")
return {}, errors
seen_families: set[str] = set()
for family_index, family_raw in enumerate(families):
family_context = f"{rel_path}:families[{family_index}]"
if not isinstance(family_raw, dict):
errors.append(f"{family_context} must be an object")
continue
unknown_family_keys = sorted(set(family_raw) - {"name", "entries"})
if unknown_family_keys:
errors.append(
f"{family_context} has unknown keys: {', '.join(unknown_family_keys)}"
)
family_name = _require_non_empty_string(
family_raw.get("name"), f"{family_context}.name", errors
)
if family_name is None:
continue
if family_name in seen_families:
errors.append(f"duplicate blocklist family: {family_name}")
continue
seen_families.add(family_name)
if family_name not in MANAGED_FAMILY_NAMES:
errors.append(f"unknown blocklist family: {family_name}")
continue
entries = family_raw.get("entries")
if not isinstance(entries, list):
errors.append(f"{family_context}.entries must be a list")
continue
if not entries:
errors.append(f"{family_context}.entries must not be empty")
continue
family_entries: dict[str, BlocklistEntry] = {}
for entry_index, entry_raw in enumerate(entries):
entry_context = f"{family_context}.entries[{entry_index}]"
if not isinstance(entry_raw, dict):
errors.append(f"{entry_context} must be an object")
continue
unknown_entry_keys = sorted(
set(entry_raw) - {"field", "policy", "reason", "tracking_issue"}
)
if unknown_entry_keys:
errors.append(
f"{entry_context} has unknown keys: {', '.join(unknown_entry_keys)}"
)
field_name = _require_non_empty_string(
entry_raw.get("field"), f"{entry_context}.field", errors
)
policy = _require_non_empty_string(
entry_raw.get("policy"), f"{entry_context}.policy", errors
)
reason = _require_non_empty_string(
entry_raw.get("reason"), f"{entry_context}.reason", errors
)
tracking_issue_raw = entry_raw.get("tracking_issue")
tracking_issue: str | None = None
if tracking_issue_raw is not None:
tracking_issue = _require_non_empty_string(
tracking_issue_raw, f"{entry_context}.tracking_issue", errors
)
if field_name is None or policy is None or reason is None:
continue
if policy not in BLOCKLIST_POLICIES:
errors.append(
f"{entry_context}.policy must be one of: "
f"{', '.join(sorted(BLOCKLIST_POLICIES))}"
)
continue
if field_name in family_entries:
errors.append(f"duplicate blocklist entry: {family_name}.{field_name}")
continue
family_entries[field_name] = BlocklistEntry(
family_name=family_name,
field_name=field_name,
policy=policy,
reason=reason,
tracking_issue=tracking_issue,
)
if family_entries:
blocklist[family_name] = family_entries
return blocklist, errors
def iter_json_docs(text: str) -> Iterable[dict]:
decoder = JSONDecoder()
index = 0
length = len(text)
while index < length:
while index < length and text[index].isspace():
index += 1
if index >= length:
return
obj, index = decoder.raw_decode(text, index)
yield obj
def _find_clang_binary() -> str:
"""Return the clang++ binary to use for AST dumping.
Resolution order:
1. a clang token from $CXX -- which may be empty, a single binary, or
compiler-launcher-prefixed and/or versioned (e.g. "ccache clang++-21");
we use the clang word itself (a bare compiler, not the launcher, since
we run -ast-dump rather than compile)
2. bare 'clang++'
3. versioned fallbacks, newest first
Raises RuntimeError if none found.
"""
import os
cxx = os.environ.get("CXX", "")
for token in cxx.split():
if "clang" in token and shutil.which(token):
return token
for candidate in (
"clang++",
"clang++-21",
"clang++-20",
"clang++-19",
"clang++-18",
"clang++-17",
"clang++-16",
"clang++-15",
"clang++-14",
"clang++-13",
):
if shutil.which(candidate):
return candidate
raise RuntimeError(
"clang++ not found. Install clang or set CXX to a clang++ binary. "
"Alternatively, run tools/c_api_gen/regen_all.py on a machine "
"with clang++ and commit the resulting .inc files."
)
_CLANG_BINARY_CACHE: list[str] = []
def get_clang_binary() -> str:
if not _CLANG_BINARY_CACHE:
_CLANG_BINARY_CACHE.append(_find_clang_binary())
return _CLANG_BINARY_CACHE[0]
def walk_ast(node: dict) -> Iterable[dict]:
yield node
for child in node.get("inner", []):
yield from walk_ast(child)
def discover_fields(family: FamilyConfig) -> list[tuple[str, str]]:
cmd = [
get_clang_binary(),
"-std=c++20",
"-Wno-pragma-once-outside-header",
"-I.",
"-I./include",
"-x",
"c++",
"-Xclang",
"-ast-dump=json",
"-Xclang",
f"-ast-dump-filter={family.struct_name}",
"-fsyntax-only",
str(family.header.relative_to(ROOT)),
]
proc = subprocess.run(
cmd,
cwd=ROOT,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
for doc in iter_json_docs(proc.stdout):
for node in walk_ast(doc):
if (
node.get("kind") in ("RecordDecl", "CXXRecordDecl")
and node.get("name") == family.struct_name
and node.get("completeDefinition")
):
fields = []
for child in node.get("inner", []):
if child.get("kind") == "FieldDecl":
fields.append((child["name"], child["type"]["qualType"]))
return fields
raise RuntimeError(f"Could not find AST definition for {family.struct_name}")
def collect_function_names(root_file: Path, include_re: re.Pattern[str]) -> set[str]:
names = set(FUNCTION_RE.findall(root_file.read_text()))
for rel in include_re.findall(root_file.read_text()):
include_path = ROOT / Path(rel)
if include_path.resolve() in AUTO_OUTPUTS or not include_path.exists():
continue
names.update(FUNCTION_RE.findall(include_path.read_text()))
return names
def collect_existing_bindings() -> tuple[set[str], set[str]]:
decls = collect_function_names(TEMPLATE_ROOT / "c_base.h", HEADER_INCLUDE_RE)
defs = collect_function_names(TEMPLATE_ROOT / "c_base.cc", SOURCE_INCLUDE_RE)
return decls, defs
def normalize_token(token: str) -> str:
if len(token) > 1 and token.isupper():
return token
return token.lower()
def to_c_suffix(name: str) -> str:
tokens = TOKEN_RE.findall(name)
return "_".join(normalize_token(token) for token in tokens)
def lookup_override(overrides: tuple[tuple[str, str], ...], field_name: str) -> str | None:
for override_field_name, override_name in overrides:
if override_field_name == field_name:
return override_name
return None
def setter_name(family: FamilyConfig, field_name: str) -> str:
override = lookup_override(family.setter_name_overrides, field_name)
if override is not None:
return override
return f"{family.c_prefix}_set_{to_c_suffix(field_name)}"
def getter_name(family: FamilyConfig, field_name: str) -> str:
override = lookup_override(family.getter_name_overrides, field_name)
if override is not None:
return override
return f"{family.c_prefix}_get_{to_c_suffix(field_name)}"
def scalar_return_type(qual_type: str) -> str | None:
if qual_type in (
"uint8_t",
"uint32_t",
"uint64_t",
"int",
"int64_t",
"unsigned int",
"size_t",
"double",
):
return qual_type
if qual_type == "SequenceNumber":
return "uint64_t"
return None
def build_option_specs(family: FamilyConfig, field_name: str, qual_type: str) -> list[dict] | None:
expr = family.field_expr_format.format(
receiver_name=family.c_receiver_name, field_name=field_name
)
setter = setter_name(family, field_name)
getter = getter_name(family, field_name)
if qual_type == "bool":
c_type = "unsigned char"
return [
{
"kind": "field_setter",
"name": setter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"value_type": c_type,
"value_name": "v",
"field": expr,
},
{
"kind": "field_getter",
"name": getter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": c_type,
"expr": expr,
},
]
scalar_type = scalar_return_type(qual_type)
if scalar_type is not None:
return [
{
"kind": "field_setter",
"name": setter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"value_type": scalar_type,
"value_name": "v",
"field": expr,
},
{
"kind": "field_getter",
"name": getter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": scalar_type,
"expr": expr,
},
]
if qual_type == "std::string":
return [
{
"kind": "field_setter",
"name": setter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"value_type": "const char*",
"value_name": "v",
"field": expr,
},
{
"kind": "string_field_getter",
"name": getter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"len_type": "size_t*",
"len_name": "size",
"expr": expr,
},
]
if qual_type == "std::chrono::microseconds":
return [
{
"kind": "field_setter",
"name": setter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"value_type": "uint64_t",
"value_name": "microseconds",
"field": expr,
"value_expr": "std::chrono::microseconds(microseconds)",
},
{
"kind": "field_getter",
"name": getter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": "uint64_t",
"expr": f"{expr}.count()",
},
]
if qual_type in family.enum_types:
return [
{
"kind": "field_setter",
"name": setter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"value_type": family.enum_c_type,
"value_name": "v",
"field": expr,
"value_expr": f"static_cast<decltype({expr})>(v)",
},
{
"kind": "field_getter",
"name": getter,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": family.enum_c_type,
"expr": f"static_cast<{family.enum_c_type}>({expr})",
},
]
return None
def build_metadata_specs(
family: FamilyConfig, field_name: str, qual_type: str
) -> list[dict] | None:
expr = family.field_expr_format.format(
receiver_name=family.c_receiver_name, field_name=field_name
)
fn_name = f"{family.c_prefix}_{to_c_suffix(field_name)}"
if qual_type == "std::string":
return [
{
"kind": "string_field_getter",
"name": fn_name,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"len_type": "size_t*",
"len_name": "size",
"expr": expr,
}
]
if qual_type == "Status":
return [
{
"kind": "status_getter",
"name": fn_name,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"expr": expr,
}
]
if qual_type == "bool":
return [
{
"kind": "field_getter",
"name": fn_name,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": "unsigned char",
"expr": expr,
}
]
scalar_type = scalar_return_type(qual_type)
if scalar_type is not None:
return [
{
"kind": "field_getter",
"name": fn_name,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": scalar_type,
"expr": expr,
}
]
if qual_type in family.enum_types:
return [
{
"kind": "field_getter",
"name": fn_name,
"receiver_type": family.c_receiver_type,
"receiver_name": family.c_receiver_name,
"return_type": family.enum_c_type,
"expr": f"static_cast<{family.enum_c_type}>({expr})",
}
]
return None
def apply_abi_type_overrides(specs: list[dict] | None) -> list[dict] | None:
"""Pin the public C type of already-shipped functions for ABI stability.
The generated body still assigns/returns through the real C++ field type
via static_cast, so only the C signature is held stable. See
abi_type_overrides.json.
"""
if not specs:
return specs
for spec in specs:
c_type = ABI_TYPE_OVERRIDES.get(spec.get("name", ""))
if c_type is None:
continue
if spec["kind"] == "field_setter":
spec["value_type"] = c_type
# Cast the (ABI-pinned) C value to the actual C++ field type.
spec["value_expr"] = (
f"static_cast<decltype({spec['field']})>({spec['value_name']})"
)
elif spec["kind"] == "field_getter":
spec["return_type"] = c_type
spec["expr"] = f"static_cast<{c_type}>({spec['expr']})"
else:
raise ValueError(
f"ABI type override for {spec['name']} targets unsupported "
f"spec kind {spec['kind']!r}"
)
return specs
def build_specs(family: FamilyConfig, field_name: str, qual_type: str) -> list[dict] | None:
if family.mode == "options":
return apply_abi_type_overrides(
build_option_specs(family, field_name, qual_type)
)
if family.mode == "metadata":
return apply_abi_type_overrides(
build_metadata_specs(family, field_name, qual_type)
)
raise ValueError(f"Unknown family mode: {family.mode}")
def regen_comment_lines(output_group: OutputGroup) -> list[str]:
return [
"// To regenerate this file:",
"// python3 tools/c_api_gen/auto_simple_bindings.py",
"",
f"// Output group: {output_group.name}",
"",
]
def auto_header_banner(output_group: OutputGroup) -> list[str]:
sources = sorted(
{
str(family.header.relative_to(ROOT))
for family in output_group.families
}
)
lines = [
"// @generated",
"// -----------------------------------------------------------------------------",
"// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.",
"// DO NOT EDIT THIS FILE DIRECTLY.",
"// Sources:",
]
for source in sources:
lines.append(f"// - {source}")
lines.extend(
[
f"// - {display_path(BLOCKLIST_PATH)}",
"// - include/rocksdb/c.h",
"// - tools/c_api_gen/c_base.cc",
"// -----------------------------------------------------------------------------",
"",
]
)
return lines
def render_group(output_group: OutputGroup, functions_by_family: dict[str, list[dict]], mode: str) -> str:
lines: list[str] = []
lines.extend(auto_header_banner(output_group))
lines.extend(regen_comment_lines(output_group))
for family in output_group.families:
section_name = family.section_name or family.struct_name
lines.append(f"/* {section_name} */")
lines.append("")
render = render_decl if mode == "header" else render_impl
for fn in functions_by_family[family.struct_name]:
lines.extend(render(fn))
lines.append("")
return "\n".join(lines).rstrip() + "\n"
def classify_function_coverage(
fn_name: str, decls: set[str], defs: set[str], errors: list[str]
) -> str:
in_decl = fn_name in decls
in_def = fn_name in defs
if in_decl and in_def:
return "covered"
if in_decl and not in_def:
return "missing_def"
if not in_decl and not in_def:
return "missing_both"
errors.append(
f"partial binding coverage: {fn_name} "
f"(declared={in_decl}, defined={in_def})"
)
return "partial"
def generate_groups(blocklist_path: Path) -> tuple[dict[Path, str], list[str]]:
blocklist, errors = load_blocklist(blocklist_path)
decls, defs = collect_existing_bindings()
outputs: dict[Path, str] = {}
for output_group in OUTPUT_GROUPS:
header_generated: dict[str, list[dict]] = {
family.struct_name: [] for family in output_group.families
}
source_generated: dict[str, list[dict]] = {
family.struct_name: [] for family in output_group.families
}
for family in output_group.families:
fields = discover_fields(family)
blocked_fields = blocklist.get(family.struct_name, {})
discovered_field_names = {field_name for field_name, _ in fields}
for blocked_field_name in sorted(blocked_fields):
if blocked_field_name not in discovered_field_names:
errors.append(
"stale blocklist entry: "
f"{family.struct_name}.{blocked_field_name} not found in "
f"{family.header.relative_to(ROOT)}"
)
for field_name, qual_type in fields:
if field_name in blocked_fields:
continue
specs = build_specs(family, field_name, qual_type)
if specs is None:
errors.append(
"unsupported field in managed family: "
f"{family.struct_name}.{field_name} ({qual_type}); "
f"extend auto_simple_bindings.py or add an entry to "
f"{display_path(blocklist_path)}"
)
continue
for spec in specs:
coverage = classify_function_coverage(
spec["name"], decls, defs, errors
)
if coverage == "missing_both":
header_generated[family.struct_name].append(spec)
source_generated[family.struct_name].append(spec)
elif coverage == "missing_def":
source_generated[family.struct_name].append(spec)
outputs[output_group.header_out] = render_group(
output_group, header_generated, "header"
)
outputs[output_group.source_out] = render_group(
output_group, source_generated, "source"
)
return outputs, errors
def format_generated_output(path: Path, content: str) -> str:
if CLANG_FORMAT is None:
return content
result = subprocess.run(
[CLANG_FORMAT, "--assume-filename", str(path)],
input=content,
text=True,
capture_output=True,
check=True,
cwd=ROOT,
)
return result.stdout
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--check",
action="store_true",
help="fail instead of writing if outputs are stale",
)
parser.add_argument(
"--blocklist",
type=Path,
default=BLOCKLIST_PATH,
help="path to the checked-in auto-binding blocklist JSON",
)
args = parser.parse_args()
blocklist_path = args.blocklist
if not blocklist_path.is_absolute():
blocklist_path = ROOT / blocklist_path
outputs, errors = generate_groups(blocklist_path)
if errors:
for error in errors:
print(f"error: {error}", file=sys.stderr)
return 1
outputs = {
path: format_generated_output(path, content)
for path, content in outputs.items()
}
if args.check:
stale = [
str(path)
for path, content in outputs.items()
if not path.exists() or path.read_text() != content
]
if stale:
for path in stale:
print(f"stale generated file: {path}", file=sys.stderr)
return 1
return 0
for path, content in outputs.items():
write_if_changed(path, content)
return 0
if __name__ == "__main__":
raise SystemExit(main())