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
1305 lines
45 KiB
Python
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())
|