C ABI of rgpot-core¶
rgpot-core/include/rgpot.h carries two identities.
The package version
(RGPOT_VERSION, RGPOT_VERSION_MAJOR, RGPOT_VERSION_MINOR, RGPOT_VERSION_PATCH)
follows the crate.
The ABI stamp follows the layout of the exported structs and the signatures of
the exported functions, and does not move with the package version.
A consumer that loads librgpot at run time checks the stamp.
Stamp¶
typedef struct rgpot_abi_stamp_t {
uint16_t abi_major;
uint16_t abi_minor;
uint16_t layout_revision;
} rgpot_abi_stamp_t;
#define RGPOT_ABI_VERSION_MAJOR 1
#define RGPOT_ABI_VERSION_MINOR 0
#define RGPOT_ABI_LAYOUT_REVISION 1
rgpot_abi_stamp_t rgpot_abi_stamp(void);
int32_t rgpot_abi_compatible(const rgpot_abi_stamp_t *stamp);
The macros hold the stamp of the header the caller compiled against.
rgpot_abi_stamp() returns the stamp of the library actually loaded.
rgpot_abi_compatible(&stamp) returns nonzero when the loaded library can serve
a caller built with stamp: equal abi_major, equal layout_revision, and a
library abi_minor at least the caller’s.
A null pointer returns 0.
rgpot_abi_stamp_t built = {RGPOT_ABI_VERSION_MAJOR, RGPOT_ABI_VERSION_MINOR,
RGPOT_ABI_LAYOUT_REVISION};
if (!rgpot_abi_compatible(&built)) {
rgpot_abi_stamp_t have = rgpot_abi_stamp();
fprintf(stderr, "librgpot ABI %u.%u layout %u, built against %u.%u layout %u\n",
have.abi_major, have.abi_minor, have.layout_revision,
built.abi_major, built.abi_minor, built.layout_revision);
abort();
}
Bumping the numbers¶
The numbers live in rgpot-core/src/version.rs as
RGPOT_ABI_VERSION_MAJOR, RGPOT_ABI_VERSION_MINOR and
RGPOT_ABI_LAYOUT_REVISION.
Regenerate the header with pixi run gen-header and commit it with the change.
The crate test committed_header_matches_the_crate fails when the header and
the source disagree.
abi_major: increment for a change a caller built against the old header cannot survive: removing or renaming an exported function, changing a signature, changing the meaning of an existing argument or status code. Resetabi_minorto 0.abi_minor: increment for an additive change that leaves every old call valid: a new exported function, a new status code, a new enum value a caller may ignore. A caller built against minor N runs on any library of the same major and layout revision whose minor is at least N.layout_revision: increment when the size or field order of an exported struct changes, or an existing field changes type, even when every function keeps its name. A caller and a library with different layout revisions never match, whatever their minors.
The ABI numbers do not follow the package version: a release that changes no
exported symbol leaves them where they are.
They are also not the ELF soversion of librgpot.
The C API is exported from that library and has no shared library of its own, so
a C caller relinks when the C++ SONAME moves even if these numbers stay.
3.5.0 left them at 1, 0 and 1, and they stay there: no exported C struct changed
layout.
C++ layout stamp¶
The stamp above covers the C interface of rgpot-core.
The header-only C++ classes are a second boundary: a consumer compiles
Potential, the force structures and the pair potentials, and the library
supplies their out-of-line members.
A library built from another revision of those headers, or with another value of
RGPOT_HAS_CACHE, carries the same C stamp and still runs with the wrong
offsets or the wrong vtable.
rgpot/abi/CxxLayout.hpp stamps that boundary.
It needs neither rgpot.h nor the Rust core, so every build of librgpot
exports it.
#include "rgpot/abi/CxxLayout.hpp"
if (!rgpot::abi::layout_compatible()) {
// the loaded library was built from other layouts than these headers
}
rgpot::abi::header_stamp()is aconstexprof the header: the stamp the consumer compiled against.rgpot::abi::library_stamp()calls the exportedrgpot_cxx_layout_stamp(): the stamp of the loaded library.rgpot::abi::layout_compatible()is true when the revision, the feature bits and the hash are equal.rgpot_cxx_layout_compatible(const rgpot_cxx_layout_stamp_t *)is the same check for a caller that holds only the C struct(uint32_t revision, uint32_t features, uint64_t hash). The C stamp struct ofrgpot.hdoes not change.
The hash folds sizeof, alignof and, for plain structures, offsetof of each
covered type, plus the pointer width.
Feature bit 0 is RGPOT_HAS_CACHE, which adds a virtual to PotentialBase and
changes Potential.
Covered types:
ForceInput,ForceOut,ForceBatch,PotCaps,CalculatorGroupLJConfig,LJClusterConfig,MorseConfig,ZBLConfigReentrancyandPotType(width, and the values of the enumerators consumers switch on)PotentialBase,Potential<Derived>(through one probe instantiation),LJPot,LJClusterPot,MorsePot,ZBLPotwith the cache:
cache::EvalCountsandcache::PotentialCache
Not covered: the classes of optional backends (xtb, tblite, dftd3, dftd4,
ExprPot, XcKernel, Metatomic, UMA, NWChem, CPMD, MOPAC, Skala, the Fortran
potentials).
Their headers and features differ between builds, so a stamp over them would
refuse libraries that agree on everything a consumer uses.
The plugin interface carries
RGPOT_PLUGIN_API_VERSION, and the engine libraries carry rgpot_engine_abi_version().
Bumping¶
A change to the size, alignment or a field offset of a covered type moves the hash by itself. Bump
kCxxSoversionin the same change. That constant is the ELF soversion oflibrgpot(librgpot.so.N). Meson and CMake read it, so the SONAME is not written down anywhere else. The hash is not the SONAME. It depends on the platform tag (kCxxPlatformTag): operating system, architecture, standard library and data model. It also folds pointer width and the cache feature bit, so two builds of one revision do not share a hash. Record each no-cache hash inkCxxSoversionTableinCppCore/rgpot/abi/CxxLayout.hpp.CxxLayout.ccstaticassertsthe row for the platform it is compiled on. A platform with no row does not fail the build. The compiler prints that the tag is unlisted.CppCore/tests/check_cxx_soversion.pyfails a live hash only when that tag is in the table. It fails when the fingerprint differs fromCppCore/rgpot/abi/cxx_soversion_ledger.txt, and when a shipped ledger line changes under the same soversion. Append a line to that ledger with the new soversion and the fingerprint the check prints. The fingerprint is platform-independent. Do not edit a shipped line.kCxxLayoutRevisioninCppCore/rgpot/abi/CxxLayout.hpp: increment when a change keeps every size and offset and still alters the ABI. Reordering virtual functions, changing the meaning of a field, changing a base class, changing a default member initializer a consumer inlines. BumpkCxxSoversionin that change as well: the revision is an input of the hash.kPotentialBaseVirtuals,kPotentialBaseCacheVirtualsandkPotentialVirtualscount the virtual functions of the polymorphic classes, because the compiler hides a vtable change fromsizeof.CppCore/tests/check_virtual_counts.pycounts thevirtualdeclarations inPotential.hppand fails when a count is stale. Update the constant and incrementkCxxLayoutRevisiontogether.Adding a covered type changes the hash, so it needs no revision bump. Add it to
detail::compute_hash()in the same commit, and bumpkCxxSoversionwith the hash.
Updating the platform table¶
kCxxSoversionTable starts empty when the soversion changes, and is filled from
CI.
Do not copy a hash from another platform or from a local build.
Each CI build of the printer prints one RGPOT_CXX_LAYOUT_STAMP line.
It carries tag, hash, hash_dec, features and revision.
features is 1 for a cache build.
Ignore those lines.
The no-cache hash is the one that goes in the table.
The workflow “C++ layout stamp” compiles
CppCore/tests/print_cxx_layout_stamp.cc on Linux x8664, Linux aarch64, macOS
arm64, macOS x8664and Windows MSVC.
The Linux and macOS wheels print the same line from the compiler that builds the
wheel.
The Meson CxxSoversion test runs the printer too.
There is no musl row unless a log prints a linux-musl- tag. librgpot wheels
skip musllinux, and the musl potctl build does not compile this header.
When the soversion is bumped:
Increment
kCxxSoversion.Leave
kCxxSoversionTableempty and delete the listed-platform guards, so every platform builds and the printers run.Read each
RGPOT_CXX_LAYOUT_STAMPline whosefeaturesis 0.Add one table row per distinct tag, using that line’s
hashas0x<hash>ull. Add a matching guard. The guard is an#if, then#elif, onRGPOT_OS_*,RGPOT_ARCH_*,RGPOT_STDLIB_*andRGPOT_MODEL_*. Inside the branch, defineRGPOT_CXX_PLATFORM_LISTEDas 1 andRGPOT_CXX_PLATFORM_LISTED_TAGas that same tag string.Append one ledger line: the new soversion and the fingerprint the check prints. Do not edit a shipped line.
Push and wait until the jobs that printed the hashes are green. A listed tag whose live hash differs fails the build.
The fixture CppCore/tests/layout_fixture.cc is a shared object built from a
header in which ForceOut gained a field; CxxLayoutTest requires that its
stamp differs and that its compatibility check refuses the real stamp.
kCxxSoversion applies to librgpot only.
librgpot_mpi stays at soversion 3: its C ABI is RGPOT_MPI_ABI_VERSION 1.
The engine plugins (libclassical_engine.so, libmopac_engine.so,
libnwchem_engine.so, libskala_engine.so,
libxtb_engine.so, libmetatomic_engine.so,
libuma_engine.so) are opened by those unversioned names and keep RGPOT_ENGINE_ABI_VERSION 1.
The libpot_client_bridge header did not change.
librgpot_cuh2 only added rgpot_cuh2_force_ws.
The pair-potential types whose layouts changed are static objects inside
librgpot, so they have no SONAME of their own.
CMake’s libptlrpc is the same: the schema it carries is the 3.5.0 schema, and
Meson does not install it as a separate shared library.
Scope¶
The first stamp covers the C surface of rgpot-core.
The C++ and Python packages do not re-export it; the C++ classes have the layout
stamp above.
The engine plugins carry their own number, rgpot_engine_abi_version().
The eindir objective keeps rgpot_eindir_abi_stamp(), which reports the stamp
of eindir-core.