Skip to content

Transpile PostgreSQL C macros into Rust macro_rules! - #2389

Draft
eeeebbbbrrrr wants to merge 44 commits into
pgrx-0.20.0-developfrom
macro_rules
Draft

eeeebbbbrrrr wants to merge 44 commits into
pgrx-0.20.0-developfrom
macro_rules

Conversation

@eeeebbbbrrrr

@eeeebbbbrrrr eeeebbbbrrrr commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

This adds pgrx-c-macros, a library and clap CLI that follows cargo-pgrx’s PostgreSQL selection rules to discover and transpile function-style server macros with header and line-span provenance, supported by an architecture guide and extension book chapter.

cargo run -p pgrx-c-macros -- list pg18
cargo run -p pgrx-c-macros -- analyze pg18 --format json
cargo run -p pgrx-c-macros -- emit pg18 --name TYPEALIGN
  • During pgrx-pg-sys builds, libclang and a compatible Clang executable establish the active definitions, declarations and recorded C profile alongside fresh bindgen output, while tracked inputs trigger regeneration and optional inspection/native failures preserve ordinary bindings through an unavailable report and classifier; PGRX_C_MACROS=0 opts out explicitly.
  • The runtime admits compiler-verified LP64, LLP64 and ILP32 profiles, including unsigned-char Linux AArch64, Windows and either byte order and uses generated capability traits to preserve C promotions, rank, signedness, overflow, qualifiers, repeated evaluation and unevaluated operands, with caller-authored Rust unsafe operations and original C pointer, resource and thread obligations remaining explicit.
  • Supported forms include scalar expressions, qualified pointers, record/union fields, packed and volatile access, bitfields, mutation, function/callback calls, static inline functions, sizeof, alignment, offsetof, return adapters, supported statement blocks, compiler-verified byte swaps/branch hints and closed token pastes, with native access and ABI adapters compiled into a verified static archive.
  • Formatted cmacros/pgNN/ modules follow source-header paths, preserve parameter and verified literal/symbol/type-alias spellings, document the original C definitions and safety/panic domains, and expose macros through pgrx with public cmacros::c operand tags and if_c_macro! availability checks.
  • Demand-driven support has stable member/signature identities, complete input-byte verification avoids unoptimized repeated compression work, and release generation refreshes all documentation snapshots while the published package includes only the PG15 snapshot selected by docs.rs.
  • The dependency graph propagates binding disagreements and unavailable callees into structured skips in OUT_DIR/pgNN_macro_report.json, diagnostics require PGRX_MACRO_DEBUG=1, explanatory comments mark uncertain expansion fallbacks, and ordinary cargo tests compare generated Rust with original C for configured PostgreSQL installations.

@GijungKim GijungKim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved

Discover macro definitions from configured PostgreSQL headers with pgrx-pg-config and Clang. Provide an owned library inventory and a CLI with a required positional PostgreSQL version.
Macro discovery now scans pgrx's complete version-specific header wrapper and records physical files and line spans. The CLI and primary library inventory contain only function macros from the selected PostgreSQL server header tree, with other definitions retained as conversion context.
Inspect the selected PostgreSQL headers with matching Clang and emit a bounded integer macro_rules family during pgrx-pg-sys binding generation. Generated macros preserve C type identities, promotions, conversions, evaluation counts and overflow policy, while unsupported constructs retain source spans and skip reasons. Track compiler, flags, headers and include-search changes for regeneration. Differential fixtures compare generated Rust with original C across PostgreSQL 15–19.
Preserve verified PostgreSQL binding names, literal spellings, and direct macro calls in generated Rust macros.
Track dependencies with petgraph and skip macros and their callers when bindgen and Clang disagree on a constant, reporting both values without changing bindgen output.
Include original C definitions in documentation and ship PostgreSQL 15–19 macro snapshots for docs.rs.
Generate pointer, field, enum, callback, mutation and function-call adapters from Clang declarations and fresh bindgen output without handwritten macro signatures.
Preserve C evaluation contexts and partially initialized storage, compile native access primitives under the inspected profile, and retain PostgreSQL's FFI guard.
Expose generated adapters through the docs.rs include path and reject unsupported constructs or inconsistent bindings with dependency-aware diagnostics.
Add differential tests against original C, including the macros pgrx previously ported by hand.
Avoid recording preprocessing inventories in declaration-only Clang probes, and share snapshot checks between adjacent compiler phases. Index C and Rust storage identities once per immutable emission pass instead of once per macro. Optimize SHA-2 in debug builds so CLI input verification does not dominate runtime. Preserve compiler input checks, dependency skips, and generated output.
Generated macros now execute ordered C statements, preserve nested scopes and local initialization, and apply C assignment conversion to caller returns. Caller-scope captures are explicit operands, while guards reject local capture that Rust hygiene cannot reproduce. Compiler assertions remain skips instead of becoming runtime calls, and C-oracle tests cover sequencing, conversions, mutations, and unused arguments.
Generated macros now preserve if/else control flow, caller returns, and local initialization across every surviving branch. Unbraced conditionals require an explicit statement boundary. Original argument names and verified Rust type aliases remain visible while C rank, qualifiers, and null semantics stay intact. Refresh the documentation snapshots from the current generated bindings and isolate their native adapters from docs.rs builds.
Verify builtin prototypes and dynamic byte-swap behavior under the selected compiler profile before lowering to Rust. Preserve C argument conversions, result ranks, evaluation counts, and closed null constants, while isolating rejected intrinsics. Track canonical identities for availability-only header dependencies so unchanged symlinks pass validation and changed inputs are rejected. Refresh the PG15–19 macro snapshots and compare generated expressions with original C in standalone and binding integration tests.
Transpile named, generic, and dotted member offsets without reading objects, including packed, volatile, flexible, and unsupported value fields. Verify the C size_t rank and share layout checks across field adapters to keep generation within its source budget.
Prove the exact long(long, long) prototype and first-operand identity under the selected compiler profile. Preserve evaluation and C conversion of both operands, including side effects, and retain the existing byte-swap failure isolation. Add independent C oracles and PostgreSQL integration tests, and refresh the PG15–19 macro snapshots from host-generated bindings.
Use bounded Clang probes to resolve fixed token pastes while rejecting pastes that construct identifiers from caller operands. Track synthesized helpers and constants in the dependency graph so unsupported constructs and binding disagreements still skip every affected macro. Add independent C oracles and PG15-19 integration tests, and refresh macro snapshots from the host-generated bindings.
Move generated PostgreSQL macros into formatted modules by source header and expose them through pgrx. Share expression bodies and restrict native adapters to required capabilities while preserving C parameter names, aliases, evaluation and branch prediction hints. Reject ambiguous type applications and callback identities, propagate adapter failures, and validate fresh compiler artifacts instead of accepting stale output.
Credit pgrx-c-macros in the generated headers and describe regeneration of macros and their support code.
Publish pgrx-c-macros after pgrx-pg-config and before pgrx-bindgen, and include it in the preflight packaging check.
Remove the develop-only pull request filters from the test, packaging, and runas workflows.
Explain handwritten declarations, private helpers, modules, and tests across the C macro pipeline. Document the runtime access and conversion contracts while leaving generated snapshots unchanged.
@eeeebbbbrrrr
eeeebbbbrrrr changed the base branch from rm-13-14 to pgrx-0.20.0-develop October 4, 2026 02:47
Keep the module, item, and test documentation while removing doc comments from use declarations.
Keep formatting optional when rustup lacks rustfmt, and handle Clang dependency paths and diagnostic excerpts across platforms. Accept reviewed LTO flags while producing ordinary native objects for the Rust linker. Compile native macro helpers and the C shim together for the active PostgreSQL version to avoid duplicate header definitions, and retain incompatible-pointer rejection coverage with the current compiler diagnostic.
@eeeebbbbrrrr
eeeebbbbrrrr marked this pull request as draft October 4, 2026 03:33
Preprocess native helpers with the inspected flags before compiling PIC machine code, so backend flags cannot change header branches. Move PageSetPrunable coverage into the PostgreSQL harness and normalize Windows C stdio line endings in numeric oracle comparisons.
Describe the transpiler end to end, add extension examples, and introduce the feature in the top-level README. Enable macro warnings only with PGRX_MACRO_DEBUG=1 while preserving audit reports and tracking setting changes for rebuilds. Correct singular argument descriptions in generated documentation.
Generated pgstat_count_* expansions reference guarded PostgreSQL functions even when their test inputs short-circuit before calling them, so standalone Linux test binaries cannot link. Move the existing assertions and interrupt compile witnesses into pgrx-unit-tests while keeping pure prediction-hint checks in pgrx-pg-sys.
Select pgrx-c-macros alongside pgrx-bindgen so Cargo can resolve the local crate when testing unpublished release versions.

@eeeebbbbrrrr eeeebbbbrrrr left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review of 5d0fc41. I didn't build or run anything. I read the hand-written code in pgrx-c-macros, pgrx-bindgen and the pgrx-pg-sys runtime, and sampled the pg15–19 snapshots.

What looks right

  • Runtime: I found no path from safe code to UB. Every memory-touching entry point is an unsafe fn, and the traits it trusts are unsafe or sealed. The generated unsafe {} blocks that actually run never contain caller tokens. Native calls go through pg_guard_ffi_boundary.
  • C typing: integer promotions, the usual arithmetic conversions, literal typing, shifts and ?: match the C rules for LP64.

What blocks this

  • Builds that now fail: macro inspection errors fail the pgrx-pg-sys build, with no opt-out. Three configurations that build on develop fail here:
    • RPM/Arch CFLAGS containing -Wp,;
    • the cross-compile recipe used in CI and the book;
    • hosts without a Clang driver that matches libclang.
  • Builds that silently lose every macro: Linux aarch64, and any profile with an unmodeled -f flag. They compile, but export zero macros and print no warning.

Inline comments are tagged P0 (breaks a currently supported build), P1 (risk under specific conditions) or P2 (hardening or quality). "Latent" means the current PG15–19 output doesn't hit it. Three top-level comments cover the larger changes: failing soft, public API stability, and size and build cost.

The PR description should mention the new Clang-driver requirement and the change that passes CFLAGS to bindgen, since both affect existing users.

Comment thread pgrx-bindgen/src/build.rs
Comment thread pgrx-c-macros/src/frontend.rs
Comment thread pgrx-c-macros/src/frontend.rs
Comment thread pgrx-c-macros/src/frontend.rs
Comment thread pgrx-c-macros/src/analysis.rs
Comment thread docs/src/extension/c-macros.md Outdated
Comment thread docs/src/extension/c-macros.md
Comment thread .github/workflows/tests.yml
Comment thread Cargo.toml
Comment thread pgrx-c-macros/tests/postgres_handports.rs Outdated
@eeeebbbbrrrr

Copy link
Copy Markdown
Contributor Author

Larger change 1: make macro generation unable to break or silently empty a pgrx-pg-sys build

On develop, a pgrx-pg-sys build needs libclang and pg_config --cppflags. With this PR, every non-Windows build also needs:

  • a Clang driver whose major version matches libclang;
  • a CFLAGS profile that the analyzer accepts;
  • successful native compilation.

Any failure in those steps fails the build (build.rs:1326). Separately, bindgen now receives the full --cflags even when macros end up unavailable (build.rs:1176).

Configurations that build on develop and fail here, from reading the code (not run):

  • -Wp, in CFLAGS: RPM and Arch packages set -Wp,-D_FORTIFY_SOURCE=… (frontend.rs:2225).
  • The cross-compile recipe in CI and the book: -ccc-gcc-name is parsed as an extra input (frontend.rs:2190).
  • Missing driver: hosts with libclang but no clang-N/clang of the same major.
  • Driver config files: a driver that loads one (frontend.rs:2144).
  • Unsupported targets: ppc64le, s390x and FreeBSD still run the full inspection, even though the runtime gate can never emit for them.

Configurations that build but export no macros, with no output unless PGRX_MACRO_DEBUG=1:

  • Linux aarch64: plain char is unsigned there (support_generation.rs:113).
  • Common packaging flags: any profile with -fexceptions, -fno-plt, -ffile-prefix-map=, -fprofile-arcs, or another -f flag outside the allowlist (analysis.rs:550).

Suggested shape:

  1. Keep bindings independent of macros. Generate bindings with the pre-PR arguments, and treat macro generation as an add-on that consumes them.
  2. Fail soft. Map every inspection or native-build error to MacroOutput::unavailable(reason), plus one unconditional cargo:warning naming the reason and the report path.
  3. Check the target first. Gate inspection on the runtime-supported target set before running any compiler.
  4. Add an explicit opt-out, such as PGRX_C_MACROS=0 or a cargo feature. Consider making generation opt-in for 0.20 until the platform matrix has CI coverage: Linux x86_64/aarch64 with distro PostgreSQL packages, macOS with Xcode and Homebrew LLVM, and a cross build.
  5. Never go silent. Warn unconditionally whenever a whole profile is rejected or zero macros are emitted.

Once the cross-compile job on this PR finishes, it should confirm or refute the -ccc-gcc-name failure.

@eeeebbbbrrrr

Copy link
Copy Markdown
Contributor Author

Larger change 2: the public macro API varies by host; give users a way to deal with that

Which pgrx::NAME! macros exist depends on several things besides the PostgreSQL major version:

  • configure options. Assert is emitted only without USE_ASSERT_CHECKING, since the cassert form uses stringification and is skipped. gettext/dgettext/ngettext are emitted only without ENABLE_NLS.
  • the target. Linux aarch64 gets none.
  • the compiler flags.
  • whether bindgen and Clang agree on each referenced constant.

An extension that compiles against a pgrx-managed install can therefore fail against a distro or cassert build with only "cannot find macro X in crate pgrx". The PR's own backend tests already have to work around this with the internal __pgrx_c_classify! { @if_available … } (pgrx-unit-tests/src/tests/generated_c_macro_tests.rs:28).

Suggestions:

  • Stubs for skipped names. For every function-like macro that is skipped, emit macro_rules! NAME { ($($t:tt)*) => { compile_error!("pgrx: NAME was not generated: <reason>; see pgNN_macro_report.json") } }. This turns a missing-name error into an actionable one at no runtime cost.
  • A documented availability check. Offer a supported availability macro, such as pgrx::if_c_macro!(NAME { … }), in place of the hidden one.
  • A public home for the helper types. CValue and the C markers are required by the book's examples but live in #[doc(hidden)] pg_sys::__pgrx_c_macros.
  • Input typing.
    • pg_sys integer constants keep bindgen's u32 (e.g. BLCKSZ), which gives different results from C when mixed with signed operands. See the inline comment on expression.rs:407.
    • usize, i64 and u64 are rejected with raw trait errors.
  • Overlap with the hand-written ports. About 36 hand-written functions share names with generated macros (MAXALIGN, TYPEALIGN, GETSTRUCT, TransactionIdIsNormal, …). Their signatures differ: the functions take usize, and the macros reject it. Documenting which one is canonical would help, along with whether the ports are expected to go away.
  • Guarded calls to trivial helpers (predicted cost, not measured). Static inline helpers such as DatumGetPointer are called through guarded FFI. PG_GETARG_POINTER! therefore pays a sigsetjmp per argument fetch, which the existing hand-written paths don't. This is worth measuring before the book recommends these macros on hot paths.

@eeeebbbbrrrr

Copy link
Copy Markdown
Contributor Author

Larger change 3: package size, generated code volume, and rebuild cost

Measured or counted locally on 5d0fc41 unless noted.

Package size

  • pgrx-pg-sys/Cargo.toml has include = ["src/**/*", …], so the 179 MB of src/include/cmacros/pg15..pg19 snapshots are packaged.
  • A tar czf of the package inputs is about 9.76 MB. The merge base was about 1.78 MB, and crates.io's limit is 10 MiB.
  • The next major's snapshot will probably push it over the limit, and every user unpacks about 180 MB into ~/.cargo/registry.
  • docs.rs builds only pg15 ([package.metadata.docs.rs]). Shipping only that snapshot, by excluding cmacros/pg1[6-9], would fix both problems.

Code compiled on every build

  • The pg18 snapshot tree is about 690k lines, close to the 686k lines/39 MB a normal build writes to OUT_DIR. That is about 549k lines of macro_rules! plus about 137k lines of support items: roughly 9.6k impls and 9k const _ asserts.
  • For comparison, pg18.rs is about 2.4 MB.
  • Every pgrx user compiles this whether or not they call a macro.
  • Local build directories from earlier commits on this branch also hold a 36–51 MB pretty-printed pgNN_macro_report.json, rewritten on every build.

Build-script and rebuild cost (predicted, not measured)

  • Hashing: several verify passes per build SHA-256 every input, including the clang executable and libclang. In downstream build scripts that runs at opt-level 0 (see the inline note on Cargo.toml:90).
  • Watched directories: each PATH directory searched, libclang's directory, and SDK roots become recursive rerun-if-changed watches. A local build output here has 987 directives, including /Library/Developer/CommandLineTools/usr/lib.
  • Environment triggers: PATH and HOME are rerun-if-env-changed inputs.

Snapshot churn

  • The Inline_*, Address_* and __pgrx_inline_* names hash PATH, HOME, cwd and absolute paths.
  • Signature_* hashes the OS-versioned triple.
  • Field<N> is a global rank over all field names.
  • So snapshots differ per maintainer, and one new header field renumbers thousands of lines.

Per-call expansion

  • In pg18, the median macro's value arm is about 370 tokens and 11 generic runtime calls.
  • VARSIZE_ANY is about 3,600 tokens and 115 calls; att_addlength_datum is about 6,000 tokens and 169 calls.
  • expression.rs has a single #[inline], so debug builds execute all of these calls.

Before merging, I'd like to see:

  • cargo build --timings for pgrx-pg-sys and a small extension, plus a no-op rebuild, compared against develop;
  • the actual output of cargo package -p pgrx-pg-sys;
  • the predicted vs. measured numbers, per AGENTS.md.

Execute floating-point value checks as standard C and retain compile-time type proofs. Restrict generated-runtime oracles to supported targets while keeping portable frontend checks and adding a compiler-derived LLP64 rejection regression. Account for Clang profiles that report indeterminate floating-point evaluation under fast-math.
The native support test intentionally compiles its C oracle with the original flags, which can produce relocations rejected by Linux's default PIE linker. Disable PIE only when linking that oracle, preserving its preprocessing semantics and retaining default executable and shared-library checks for generated archives.
Require the native expression oracle to use the supported LP64 target family and independently assert its C storage and sizeof identities. Terminate generated C test sources with a newline so strict compiler rejection proves the intended constraints, while retaining portable pointer checks on Windows.
Recognize Clang's -ccc-gcc-name as an option with a separate value instead of rejecting the GCC name as an extra C input. Keep the original compiler arguments unchanged and verify AArch64 target facts against independent C probes, with regression coverage for missing values, NUL bytes, and additional inputs.
Always emit the availability classifier, including when the inspected ABI rejects every macro or target inspection is unavailable. Retain operand adapters only when emitted macros need them, so empty output requires no C runtime capabilities. Cover refused cross profiles, zero-argument exports, output bounds, and external consumers of the unavailable module tree.
Keep optional macro failures from breaking ordinary bindings while preserving verified compiler and storage facts. Model target char, integer ranks, alignment and calling conventions with original C oracle coverage. Fix macro hygiene, field access, packaged headers, input tracking and public operand documentation.
Regenerate the PG15 through PG19 bindings and macro modules together from fresh local bindings and the verified C target profile.
Add compiler-verified static inline adapters and C expression result helpers so callers use the same macro API across PostgreSQL versions.
Preserve selected binding identities, original string and metadata storage, callback contracts, compiler flags, and target ABI proofs.
Transfer complete checked target bundles, retain ordinary binding builds when macro generation is unavailable, and refresh the Linux documentation snapshots.
Use the target C long and inspected uintptr_t identities in macro integration tests so LLP64 and narrower targets retain the same semantic checks. Reuse Rust identifier validation for generated typedef aliases, with coverage for C names that are Rust keywords or an unrepresentable underscore.
…ofiles

Follow the inspected enum and timing-field storage when checking raw C values. Run byte-order checks in the PostgreSQL harness so guarded inline fallbacks are exercised safely, including PG19 Datum conversion.
Record actual inclusions in the existing macro-dump pass so valid -include and -imacros profiles reconcile with libclang. Keep canonical inclusion equality and separate availability dependencies, bound the temporary witness, and cover escaped paths, symlink changes, and output limits.
Retain one process-lifetime owner per loaded DLL path so LLVM thread-exit callbacks cannot jump into unloaded code. Preserve bindgen runtime identity and cover repeated scanner worker exits in an isolated regression.
Compare original C and emitted Rust text without the extra carriage returns inserted by Windows stdout. Preserve every record and literal carriage return, and verify spacing and empty records with an actual C program.
Use the target c_ulong representation for admitted numeric enum storage and reject the opposite storage width. Preserve separate checks for C rank, nominal enum identity, checked variants, and duplicate registrations on LP64 and LLP64 targets.
Preserve const and volatile on fundamental void types so pointer casts do not depend on incidental header declarations. Compare emitted validity macros with original C and check pointer qualification under native and Windows/Linux compiler profiles. Include structured skip reasons when the PostgreSQL oracle corpus is incomplete.
Configure Windows C stdout in binary mode before main in both execution harnesses so mixed diagnostic output compares byte for byte. Preserve literal carriage returns, CRLFs, blank records, and fixture source locations. Keep output comparisons exact and cover interleaved native and Rust writes.
Windows system headers define NULL identically at multiple locations, which caused PageIsValid to be skipped despite an established final replacement. Expand matching object dependencies through the original headers and retain every possible source span without assigning a guessed owner. Keep ambiguous public roots, function dependencies, and unresolved definitions rejected, and cover native and cross-target null semantics and restored macro definitions.
Formatting every generated file in one command exceeded the Windows process limit in temporary extension workspaces. Use bounded filename batches while preserving formatter options, optional-tool handling, source comments, and staged publication. Add long-path formatting and quoting-budget regressions, and simplify four existing bindgen expressions flagged by strict Clippy.
Clang's header writer prepends dependency entries, so newer Darwin SDKs were rejected as mismatched includes. Verify that prefix against the same invocation's effective -fdepfile-entry arguments and fingerprint both path spellings while keeping actual C include comparison exact. Decode records as a stream so duplicate paths do not accumulate.
PostgreSQL prediction macros return int under MSVC and long when they use the GNU builtin. Compare generated results with the original headers under the verified build profile, including exact rank, width, value and individual operand evaluation counts. Share the bounded original-header harness with the paste tests.
The outer module documentation already explains the verified binding identities; remove the redundant inner comment that triggers mixed_attributes_style.
@eeeebbbbrrrr

Copy link
Copy Markdown
Contributor Author

The Windows PG15 failure came from the test assuming that likely and unlikely always return C long: PostgreSQL's MSVC fallback returns int, while the GNU builtin returns long. Commit d9d3f89 now compares the generated result with the original PostgreSQL headers under the verified compiler profile, checking exact rank, signedness, width, value and separate operand evaluation counts.

I also reproduced a generation refusal with Apple Clang 21 and the current macOS SDK. Clang's header witness writer prepends explicit dependency entries such as SDKSettings.json before actual C inclusions; b9ee9d8 verifies that ordered prefix against the effective compiler arguments and fingerprints those dependencies while preserving exact agreement for C headers.

All 222 affected compiler/oracle tests and nine original-header integration tests passed locally, as did workspace formatting and strict compiler-crate Clippy. Broader integration linting still reports three existing library warnings in libpq and panic handling; the changed test fixtures have no lint warnings, and eba7ebc removes the duplicate binding-namespace documentation found during that check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants