Repository navigation
Make header-only builds work from any number of translation units - #104
Conversation
d6be769 to
8d2f89b
Compare
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
8d2f89b to
646f038
Compare
|
|
||
| add_library(integratorxx_common_ut quad_matcher.cxx) | ||
| if(INTEGRATORXX_HEADER_ONLY) | ||
| target_sources(integratorxx_common_ut PRIVATE lib_impl.cxx) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
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:
inlinewhen INTEGRATORXX_HEADER_ONLY is defined and to nothing otherwise.
the interface target; previously the option only changed the target
type, so nothing in the sources could tell the modes apart.
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.
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