Skip to content

support DLLs on Windows#75

Draft
jll63 wants to merge 130 commits into
boostorg:developfrom
jll63:feature/windll3
Draft

support DLLs on Windows#75
jll63 wants to merge 130 commits into
boostorg:developfrom
jll63:feature/windll3

Conversation

@jll63

@jll63 jll63 commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

jll63 and others added 30 commits July 4, 2026 13:39
Each of the 3 error-path tests used its own throw_error_handler +
runtime_checks registry via test_registry_<__COUNTER__>, so one TU
instantiated the heavy registry_state/compiler machinery three times.
CI (MSVC "Visual Studio 18 2026" preview toolset) has been failing
with C1060 (compiler out of heap space) / runner OOM across many test
files that stack multiple distinct registries in one TU; splitting to
one registry per TU, set via BOOST_OPENMETHOD_DEFAULT_REGISTRY,
reduces that per-TU cost.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both tests already used a plain test_registry_<__COUNTER__> (no extra
policies), so each now just uses the default registry directly. Part
of reducing the number of distinct registries instantiated per TU to
mitigate MSVC C1060 (out of heap space) / CI runner OOM on the
"Visual Studio 18 2026" preview toolset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each of the 4 custom-RTTI scenarios defined its own test_registry_<__COUNTER__>
with a distinct custom_rtti policy, so one TU instantiated the registry
compiler machinery four times. Splitting to one registry per TU, set
via BOOST_OPENMETHOD_DEFAULT_REGISTRY, continues reducing per-TU
template instantiation cost to mitigate MSVC C1060 / CI runner OOM on
the "Visual Studio 18 2026" preview toolset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Most test_*.cpp files just #include <boost/openmethod.hpp> (and
initialize.hpp/interop headers) before anything else, so a shared PCH
avoids re-parsing that large, mp11-heavy header once per test
executable (~34 of 50 test_*.cpp files, one PCH owner + REUSE_FROM for
the rest). A handful of files instead #define
BOOST_OPENMETHOD_DEFAULT_REGISTRY before the first inclusion of
core.hpp to install a custom registry; force-including a PCH that
already pulled in core.hpp would silently defeat that override, so
those files are detected by scanning for the macro and left without a
PCH.

Locally (MSVC 14.5x, Debug, clean rebuild of the `tests` target):
  -j4: 22.5s -> 9.4s  (-58%)
  -j1: 95.7s -> 34.9s (-64%)
All 109 tests still pass.

Scope is CMake only; Boost.Build (b2) drives more toolchains in CI
(Cygwin gcc, MinGW, clang-win, msvc, posix gcc/clang) and its PCH
support is more toolchain-fragile, so it's left untouched rather than
risk breaking a compiler we can't validate locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MSVC's std::tuple is expensive to instantiate (SFINAE'd constructor
overload sets, conditional-explicit machinery, comparisons), and the
library instantiated a fresh specialization per registry
(registry_state_type::policies), per BOOST_OPENMETHOD_CLASSES
(use_classes_tuple_type), and per override<...> (override::impl) in
every TU. CI on the VS 2026 preview toolset (msvc-14.51) has been
dying with C1060 (compiler out of heap space) pointing into <tuple>
instantiations.

detail::tuple is a ~40-line recursive head/tail holder with no
converting constructors, no comparisons, and no EBO machinery;
detail::get retrieves an element by type (via class-template partial
specialization, since overloaded function templates are ambiguous when
the searched type is the head). It tolerates duplicate element types,
which use_classes needs (a class may be listed twice).

The initialize()/finalize() *options* tuple deliberately remains
std::tuple: it is a documented policy-API signature.

Locally (msvc-14.50) the effect is a small, consistent reduction in
cl.exe peak working set (~3 MB per TU); wall-clock build time is
unchanged. The real target is the 14.51 preview's heap exhaustion.

All 109 CMake tests pass; b2 test//quick passes with msvc-14.5 and
Cygwin gcc-13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the recursive head/tail detail::tuple with a flat design that
holds each element in a tuple_element<T> base class: O(1) instantiation
depth instead of O(n), and detail::get becomes a single base-class cast
instead of a recursive template chain.

Base classes must be unique, so the two element lists that may contain
duplicates are deduplicated with mp_unique before instantiating the
tuple: use_classes_tuple_type (a class listed twice in one
BOOST_OPENMETHOD_CLASSES, deduped before inheritance_map so base lists
are deduped too) and method::override::impl (override<f, f>). The
policy-state tuple needs no dedupe: state types are distinct by
construction.

Verified: duplicate class listing and duplicate overrider registration
both still compile and dispatch correctly (gcc-13); 109/109 CMake
tests pass; b2 test//quick passes with msvc-14.5 and Cygwin gcc-13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each compile-fail test runs `cmake --build` on the shared build tree
as its test command. Under `ctest -j N`, concurrent build-tool
invocations race on shared files: with the Visual Studio generator,
every MSBuild invocation rewrites the same
ZERO_CHECK.tlog/ZERO_CHECK.lastbuildstate, and the losers abort with
MSB3491 ("file is being used by another process") before compiling
anything, so the expected diagnostic never appears in the output and
the test fails randomly. Other generators race analogously on the
cmake-regeneration step.

A RESOURCE_LOCK makes ctest run the compile-fail tests one at a time
while still running them in parallel with the ordinary tests, which do
not touch the build tree. Verified: 3 consecutive `ctest -j 12` runs
of the compile_fail subset all pass (previously ~half failed each
run), and the full 109-test suite passes at -j 12.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root cause of the windows-2025-vs2026 CI failures (C1060 out of heap
space, dead runners), found by bisection with a local 14.51 install:
when MSVC evaluates the call-expression SFINAE probe
decltype(T::initialize(declval<Args>()...)) for a T with no such
member, it wrongly instantiates the body of the same-named
enclosing-namespace function template boost::openmethod::initialize
(for its auto return type), even though a namespace-scope function is
never a candidate for a T::-qualified call. That body constructs a
compiler<Options...> and re-enters the probe with new types, so
instantiation recurses with ever-growing arguments. MSVC 19.29 is
correct; 19.44 stops with C1202; 19.51 (GA) has lost the guard rail
and consumes all memory (>16 GB per TU) before dying with C1060 -
which is what killed the CI runners.

The address-of form of the probe, already used for _MSC_VER <= 1950,
only involves T's members and is immune; extend it to all MSVC
versions instead of assuming 1951+ would be fixed. has_finalize gets
the same treatment: it was generated by the generic call-expression
macro on all compilers, and the enclosing boost::openmethod::finalize
function template sets the same trap.

Verified on MSVC 19.51.36248: a bare initialize() TU compiles
instantly (was >16 GB / C1060), the full CMake suite builds and all
105 tests pass, and b2 test//quick passes with msvc (14.51) and
Cygwin gcc-13. Standalone 30-line repros (no Boost) demonstrating the
compiler bug on 19.44/19.51, accepted by 19.29/gcc/clang, are ready
for a Microsoft Developer Community report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Switch the has_initialize/has_finalize SFINAE workaround guards to
BOOST_MSVC (Boost.Config's alias for _MSC_VER), matching Boost's own
convention. Add a #pragma message, gated on BOOST_MSVC > 1951 (the
last version confirmed to need the workaround), reminding maintainers
to re-check the underlying compiler bug on any newer MSVC before
removing the workaround. Silent on all currently-known-bad versions;
verified it fires when the guard condition is met and stays silent on
the real 19.51.36248 compiler.

Also documents measured compile-time findings for detail::tuple (kept
per discussion): a real but modest, size-dependent win over std::tuple
(~11% faster MSVC front-end time at 40 classes in a
BOOST_OPENMETHOD_CLASSES list, ~7% at 100), well below the noise floor
for this library's own test suite (2-5 classes per list), and not the
cause of the C1060/CI-OOM issue that prompted introducing it - that
was the SFINAE probe bug fixed above.

Verified: full CMake suite (105/105) on MSVC 19.51.36248, b2
test//quick on Cygwin gcc-13.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The RESOURCE_LOCK that serialized the compile-fail tests exists to avoid the
MSBuild ZERO_CHECK race, which only happens with the Visual Studio generator.
Guard it on CMAKE_GENERATOR so Ninja/Makefile builds run these tests in
parallel with each other.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dynamic_loading test forced CXX_VISIBILITY_PRESET=default on its modules to
keep the shared registry-state symbol (registry_state<...>::st) exported. Under
-fvisibility=hidden (e.g. the super-project's BoostRoot.cmake) each module
implicitly instantiates its own COMDAT copy of st, which GCC internalizes to a
per-module local symbol, so cross-DSO state sharing breaks and no symbol-level
attribute alone can fix it.

Extend the existing explicit-instantiation export/import mechanism from _WIN32
to ELF: BOOST_SYMBOL_EXPORT is visibility("default") and BOOST_SYMBOL_IMPORT is
empty there, so the same EXPORT/IMPORT macros emit a single strong,
default-visibility explicit instantiation that other modules import. This makes
the state shareable under hidden visibility, so the test no longer needs to
force visibility. Behavior is unchanged on Windows and for ELF users who don't
use the macros (non-hidden builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extending the explicit-instantiation import of the registry state to ELF/Mach-O
made the overrider reference registry_state<...>::st as an undefined (imported)
symbol instead of instantiating its own copy. The b2 Jamfile linked lib_registry
into the overrider only on Windows/Cygwin; lib_method only imports the symbol
too, so it cannot satisfy the reference. On ELF the undefined symbol resolves
lazily at load time, but Mach-O's two-level namespace requires it to be provided
by a directly linked module at link time, so the macOS build failed with
"Undefined symbols: registry_state<...>::st, referenced from overrider.o".

Link lib_registry into the overrider unconditionally. The CMake build already
gets this transitively (lib_method links lib_registry PUBLIC).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…warning

b2's scanner treats ':' as special and warned "Unescaped special character in
argument" for the <define>BOOST_OPENMETHOD_DEFAULT_REGISTRY=::boost::... value.
Escape each colon as \: ; the value passed to the compiler is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change meet to return a pair {own_word, next_word}, where next_word is
next(a, b).first when a next overrider exists, else "n/a". The dynamically
loaded Dog,Dog overrider's next now resolves — through the multi-dispatch
table — to the Animal,Animal overrider in the separately compiled method
library, exercising next across a shared-library boundary:
  meet(dog, dog) -> {"wag tails", "ignore"}   (Dog,Dog, with cross-module next)
  meet(animal..) -> {"ignore", "n/a"}         (Animal,Animal, no next)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `method_call_meet` / `overrider_call_meet` extern "C" entry points
returned `greeting` (a std::pair<std::string,std::string>) by value.
Under clang -Werror this trips -Wreturn-type-c-linkage ("returns
user-defined type ... incompatible with C"), which broke the macOS
sanitizer CI job.

Return the result through an out-parameter instead, matching the
existing method_make_dog entry point, and update main.cpp's meet_fn
signature and call sites accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix -Wreturn-type-c-linkage in dynamic_loading test
…_vector

Policies are initialized in policy_list order (left to right) and, since
vptr_vector::initialize now reads the type_hash policy's state instead of
driving its initialize(), a type_hash policy must appear before vptr_vector.
This contract was undocumented and unchecked.

- Document the left-to-right initialize / reverse finalize order on the free
  initialize()/finalize() functions, detail::initialize_policies, and the
  registries_and_policies reference page; fix that page's stale default_registry
  example (it showed vptr_vector before fast_perfect_hash).
- Make registry::finalize actually iterate in reverse policy_list order (it was
  forward), mirroring initialize, and fix the missed .st rename
  (static_::st.dispatch_data / static_::st.initialized) that prevented finalize
  from compiling at all.
- static_assert in vptr_vector::fn that, when a type_hash policy is present, it
  precedes vptr_vector in policy_list, so mis-ordered custom registries fail to
  compile instead of corrupting the vptr table at runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-add the finalize test dropped when test_dispatch.cpp was split: initialize a
registry, assert the vptr_vector state is populated, call finalize(), assert it
was cleared. Adapted to the new API — the vptr vector is reached via
Registry::state<policies::vptr_vector>().vptrs, and the has_finalize probe uses
the portable detail::has_finalize (has_finalize_aux is MSVC-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add test_registries_do_not_share_vptr_state: two distinct registries, both
using vptr_vector, have separate vptr_vector state. Asserts the two state
vectors are distinct objects and that finalizing one clears its vector while
the other's is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… address

same_ids() never compared the registry-state address and its per-policy loop
compiled to nothing (no policy defines id() anymore), so it always returned
true — the test that exists to prove the registry state is a single shared
symbol across modules could not fail. Collapse the vestigial id-array machinery
to the one meaningful value: get_ids() now returns the state address directly
(const void*), and same_ids() compares the two addresses.

Verified by building/running the dll-owner default variant (passes) and a
negative build forcing the comparison false (both same_ids checks then fail),
confirming the assertion is now load-bearing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A full URL-encoded Cygwin download mirror (setup.ini + ~157 binary tarballs) and
a scratch t.txt were committed to the repo root by mistake. They do not belong
in the library. notes.txt is kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings in: init/finalize ordering docs + reverse finalize + type_hash-before-
vptr_vector static_assert, restored finalize test, two-registry no-share test,
same_ids shared-state fix, stderr stream() doc, and removal of the accidentally-
committed Cygwin mirror + t.txt.
…ze_policies

- vptr_vector.hpp: collapse a leftover identical if constexpr(has_indirect_vptr)
  block (both arms set max_index = st().vptrs.size()); extract the type_hash-
  before-vptr_vector static_assert into named type_hash_pos/vptr_vector_pos
  intermediates for readability (same compile-time computation).
- initialize.hpp: add detail::finalize_policies (mirror of initialize_policies)
  so finalize() delegates instead of open-coding the reverse mp_for_each loop;
  reverse order now has one named home.
- test_compiler.cpp: drop the m1/m2 named refs + (void) casts for plain
  (void)method<...>::fn; discard statements.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…id family)

get_ids() now returns a single registry-state address, so the plural name was a
misnomer. Rename the helper to registry_state_id() and the exported wrappers to
method_state_id / overrider_state_id (with matching Boost.DLL symbol lookups in
main.cpp). Drop the dead, unused registry_get_ids export from registry.cpp
(it would collide with the helper's new name and nothing references it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unused, unincluded leftover with the old const void** get_ids() signature; the
live helper is registry_state_id() in registry.hpp.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

fully support dynamic loading on Windows

4 participants