Skip to content

Make header-only builds work from any number of translation units - #104

Merged
wavefunction91 merged 2 commits into
wavefunction91:masterfrom
susilehtola:fix/inline-generator-impl
Oct 8, 2026
Merged

wavefunction91 merged 2 commits into
wavefunction91:masterfrom
susilehtola:fix/inline-generator-impl

Conversation

@susilehtola

Copy link
Copy Markdown
Collaborator

The runtime generator's definitions in generators/impl/ had external
linkage, so including impl.hpp from two translation units produced
duplicate symbols. The README documented the constraint honestly --
include it "exactly once per project" -- but that is not something a
downstream project can enforce across its own dependencies.

Marking the definitions unconditionally inline does not work either: an
inline function that is not odr-used in a translation unit is never
emitted, so the precompiled libintegratorxx would export nothing and the
default build would fail to link. The two modes genuinely want different
linkage, and the whole point of the precompiled default is to keep the
large angular dispatch out of consumer translation units.

Give them different linkage explicitly:

  • INTEGRATORXX_GENERATOR_LINKAGE (impl/linkage.hpp) expands to inline
    when INTEGRATORXX_HEADER_ONLY is defined and to nothing otherwise.
  • The CMake header-only branch now defines INTEGRATORXX_HEADER_ONLY on
    the interface target; previously the option only changed the target
    type, so nothing in the sources could tell the modes apart.
  • In header-only mode the public generator headers include their own
    implementations at the end of the file, where the declarations are
    already visible. The impl headers include the public ones back, which
    #pragma once reduces to a no-op.
  • test/lib_impl.cxx existed only to instantiate the generator once for
    the header-only test build, and is no longer needed.

impl.hpp had no include guard at all; add one.

Verified both ways: the default build still exports the generator symbols
from libintegratorxx.a and passes 9/9 tests, the header-only build passes
9/9 tests, and two translation units that both include impl.hpp now link.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF

Comment thread include/integratorxx/generators/impl/linkage.hpp Outdated
@susilehtola
susilehtola force-pushed the fix/inline-generator-impl branch from d6be769 to 8d2f89b Compare September 4, 2026 07:43
The runtime generator's definitions in generators/impl/ had external
linkage, so including impl.hpp from two translation units produced
duplicate symbols. The README documented the constraint honestly --
include it "exactly once per project" -- but that is not something a
downstream project can enforce across its own dependencies.

Marking the definitions unconditionally inline does not work either: an
inline function that is not odr-used in a translation unit is never
emitted, so the precompiled libintegratorxx would export nothing and the
default build would fail to link. The two modes genuinely want different
linkage, and the whole point of the precompiled default is to keep the
large angular dispatch out of consumer translation units.

Give them different linkage explicitly:

* INTEGRATORXX_GENERATOR_LINKAGE expands to `inline` when
  INTEGRATORXX_HEADER_ONLY is defined and to nothing otherwise. It lives
  in quadrature.hpp alongside INTEGRATORXX_DISABLE_SANITY_CHECK rather
  than in a header of its own.
* The CMake header-only branch now defines INTEGRATORXX_HEADER_ONLY on
  the interface target; previously the option only changed the target
  type, so nothing in the sources could tell the modes apart.
* In header-only mode the public generator headers include their own
  implementations at the end of the file, where the declarations are
  already visible. The impl headers include the public ones back, which
  #pragma once reduces to a no-op.
* test/lib_impl.cxx existed only to instantiate the generator once for
  the header-only test build, and is no longer needed.

impl.hpp had no include guard at all; add one.

Verified both ways: the default build emits the generator symbols and
passes 9/9, the header-only build passes 9/9, and two translation units
that both include impl.hpp link.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
@susilehtola
susilehtola force-pushed the fix/inline-generator-impl branch from 8d2f89b to 646f038 Compare September 4, 2026 07:54

@wavefunction91 wavefunction91 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

See inline comments.

Comment thread test/CMakeLists.txt

add_library(integratorxx_common_ut quad_matcher.cxx)
if(INTEGRATORXX_HEADER_ONLY)
target_sources(integratorxx_common_ut PRIVATE lib_impl.cxx)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

With lib_impl.cxx gone, spherical_generator.cxx is the only test TU that includes generator headers, so nothing in CI links two of them, which is the property this PR fixes. Could we add a second TU directly to the spherical_generator executable in header-only mode, one that just includes the generator headers? Any definition that loses its inline linkage would then collide at link time. It needs to sit in the executable rather than in integratorxx_common_ut, since an object in a static library is only pulled in when something references it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added in 518c367 as test/generator_second_tu.cxx, on the executable rather than on integratorxx_common_ut for the reason you give. It only includes the three generator headers, which turns out to be enough on its own: a non-inline function with external linkage is emitted whether or not anything calls it.

Checked both directions by defining INTEGRATORXX_GENERATOR_LINKAGE to nothing in header-only mode. With the second TU the link fails on multiple definitions of angular_from_string, create_pruned_spec and robust_psi4_pruning_scheme_impl; with spherical_generator.cxx alone the same breakage still links cleanly, so the guard was indeed missing. The compiler-zoo workflow already runs both values of the option.

Comment thread README.md
To circumvent the CMake build system entirely, define
`INTEGRATORXX_HEADER_ONLY` yourself and include the generator headers as
usual. The implementations have inline linkage in this mode and may be
included from any number of translation units.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Worth stating here that the choice has to be consistent across the whole link. A TU compiled without INTEGRATORXX_HEADER_ONLY sees only the declarations, and linking it against libintegratorxx while other TUs carry the inline definitions gives the same functions two definitions with different linkage, which is an ODR violation. Something like "define it in every translation unit, and don't also link the compiled library" would cover it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added, with the ODR reasoning spelled out: define it in every translation unit and do not also link the compiled library, because a TU compiled without it sees only the declarations.

Removing lib_impl.cxx left no CI target that links two translation units
carrying the generator implementations, which is the property this PR fixes.
Add one to the spherical_generator executable: it only includes the generator
headers, which is enough, since a non-inline function with external linkage is
emitted whether or not it is called. It sits in the executable rather than in
integratorxx_common_ut because an object in a static library is pulled in only
when something references it.

Verified by defining INTEGRATORXX_GENERATOR_LINKAGE to nothing in header-only
mode: with the second TU the link fails on multiple definitions of
angular_from_string, create_pruned_spec and robust_psi4_pruning_scheme_impl,
and with spherical_generator.cxx alone it still links cleanly. The compiler-zoo
workflow already runs both values of the option, so both sides are covered.

Also state in the README that the macro has to be consistent across the whole
link, since mixing a translation unit compiled without it with the compiled
library is an ODR violation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FDTFYJMQ76iujDFNHzZyXF
@wavefunction91
wavefunction91 merged commit ddf6930 into wavefunction91:master Oct 8, 2026
8 checks passed
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