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 ~~~~~ .. code:: c 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. .. code:: c 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. Reset ``abi_minor`` to 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. .. code:: cpp #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 a ``constexpr`` of the header: the stamp the consumer compiled against. - ``rgpot::abi::library_stamp()`` calls the exported ``rgpot_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 of ``rgpot.h`` does 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``, ``CalculatorGroup`` - ``LJConfig``, ``LJClusterConfig``, ``MorseConfig``, ``ZBLConfig`` - ``Reentrancy`` and ``PotType`` (width, and the values of the enumerators consumers switch on) - ``PotentialBase``, ``Potential`` (through one probe instantiation), ``LJPot``, ``LJClusterPot``, ``MorsePot``, ``ZBLPot`` - with the cache: ``cache::EvalCounts`` and ``cache::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 ``kCxxSoversion`` in the same change. That constant is the ELF soversion of ``librgpot`` (``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 in ``kCxxSoversionTable`` in ``CppCore/rgpot/abi/CxxLayout.hpp``. ``CxxLayout.cc`` static\ :sub:`asserts`\ the 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.py`` fails a live hash only when that tag is in the table. It fails when the fingerprint differs from ``CppCore/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. - ``kCxxLayoutRevision`` in ``CppCore/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. Bump ``kCxxSoversion`` in that change as well: the revision is an input of the hash. - ``kPotentialBaseVirtuals``, ``kPotentialBaseCacheVirtuals`` and ``kPotentialVirtuals`` count the virtual functions of the polymorphic classes, because the compiler hides a vtable change from ``sizeof``. ``CppCore/tests/check_virtual_counts.py`` counts the ``virtual`` declarations in ``Potential.hpp`` and fails when a count is stale. Update the constant and increment ``kCxxLayoutRevision`` together. - Adding a covered type changes the hash, so it needs no revision bump. Add it to ``detail::compute_hash()`` in the same commit, and bump ``kCxxSoversion`` with 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 x86\ :sub:`64`\, Linux aarch64, macOS arm64, macOS x86\ :sub:`64`\ and 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: 1. Increment ``kCxxSoversion``. 2. Leave ``kCxxSoversionTable`` empty and delete the listed-platform guards, so every platform builds and the printers run. 3. Read each ``RGPOT_CXX_LAYOUT_STAMP`` line whose ``features`` is 0. 4. Add one table row per distinct tag, using that line's ``hash`` as ``0xull``. Add a matching guard. The guard is an ``#if``, then ``#elif``, on ``RGPOT_OS_*``, ``RGPOT_ARCH_*``, ``RGPOT_STDLIB_*`` and ``RGPOT_MODEL_*``. Inside the branch, define ``RGPOT_CXX_PLATFORM_LISTED`` as 1 and ``RGPOT_CXX_PLATFORM_LISTED_TAG`` as that same tag string. 5. Append one ledger line: the new soversion and the fingerprint the check prints. Do not edit a shipped line. 6. 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``.