Skip to content

Case 150: Bidirectional Export โ†” Declaration Pair

Field Value
Verdict ๐ŸŸข COMPATIBLE
Category Quality (Compatible)
Platforms Linux
Flags Bad practice
Detected ChangeKinds exported_not_public, public_not_exported
Source files examples/case150_xcheck_export_public_pair/

Category: Quality (Audit) | Verdict: ๐ŸŸข COMPATIBLE (bad practice)

Verdict and consumer impact

Single-release audit: one build's evidence checked against itself, no baseline. abicheck's verdict is COMPATIBLE โ€” the ABI hasn't broken โ€” but the audit flags two advisory findings that are the two failure directions of the same L0-exports โ†” L2-decls contract:

Direction Symptom Cross-check
exported, undeclared internal() is in the binary's export table but no public header declares it exported_not_public
declared, unexported public_api() is declared in include/demo/api.h, but a stray static kept its definition out of the export table public_not_exported

A consumer reading only the public headers believes public_api() is callable โ€” it isn't, the symbol doesn't exist in the .so. A consumer poking at the exported symbol table finds internal() โ€” nothing documents it, so any layout or behavior change to it is invisible in the public API surface. Both are contract mismatches between what the library documents and what it ships, and this case trips both directions in one build.

What this snapshot contains

snapshot.abi.json is a single, hand-built AbiSnapshot for one build of libdemo.so, carrying both the binary's export table and the public-header declaration set:

Source in the snapshot What it records
Binary export table (L0, elf.symbols) _Z8internalv (internal) exported with default visibility; _Z10public_apiv (public_api) absent
Public-header AST (L2, functions[]) public_api declared in the public header; internal not declared anywhere public

abicheck command

abicheck scan snapshot.abi.json

Expected abicheck finding

Coverage
  crosscheck:exported_not_public present   binary exports โ†” public headers: 1 of 1 export(s) undocumented (0 accounted as documented API / compiler artifact); by reason: undeclared_export=1
  crosscheck:public_not_exported present   public headers โ†” binary exports: 1 declaration(s) with an export obligation the binary does not satisfy

ABI-hygiene catalog (intra-version, advisory)
  [warning] exported_not_public: 1
  [warning] public_not_exported: 1

Verdict: COMPATIBLE (exit 0)

Minimum evidence

min_evidence: L2 โ€” the binary export table (L0) alone sees only which symbols exist, with no notion of what's "public"; the public-header AST (L2) is what supplies the declared-API boundary that exported_not_public and public_not_exported cross-check the export table against.

Why abicheck catches it

Each direction needs both sources: the binary export set and the public-header declaration set. exported_not_public flags an export with no matching public declaration; public_not_exported flags a public declaration with no matching export. Neither check can fire from either source alone โ€” the export table has no notion of "declared", and the header AST has no notion of "exported" โ€” the cross-check is what makes the mismatch visible, in both directions symmetrically.

Why this matters for a real release

internal() shipping in the export table without a public declaration means the maintainer can change or remove it without warning โ€” but some consumer, reading the .so's symbol table directly (common with dlsym or reverse-engineered bindings), may already depend on it as if it were stable. public_api() being documented but absent from the exports is the opposite failure: any consumer that follows the header and calls it gets a link error the moment they try, not a silent bug โ€” but it means the library's own documented contract doesn't match what it ships, caught here before a consumer files that bug report.

Safe redesign

  • internal(): hide it (version-script local: scoping or hidden visibility) or add a public declaration for it if it's genuinely meant to be callable.
  • public_api(): remove the stray static so the definition is actually exported, or drop the declaration from the public header if it was never meant to ship.

Cross-tool comparison

The exported_not_public / public_not_exported pair is a cross-source check unique to abicheck's audit mode โ€” it reconciles a build's own export table against its own public-header declaration set within the same build, which isn't something abidiff/abi-compliance-checker do (they diff two ABI dumps against each other, not a binary's exports against its own headers).


Source files

  • snapshot.abi.json

See also: Examples overview ยท All COMPATIBLE cases ยท Category: Quality (Compatible).