Skip to content

support DLLs on Windows#75

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

support DLLs on Windows#75
jll63 wants to merge 148 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 22 commits July 6, 2026 06:02
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>
test_finalize_clears_vptr_vector used ADD_METHOD(A), which binds `auto& m_A`
that the test never references, tripping -Werror=unused-variable on the b2 CI
jobs (MinGW/posix build with -Wall -Wextra -Werror). Register the method with a
discard-value expression instead, matching test_registries_do_not_share_vptr_state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removing the dead registry_get_ids export left registry_state_id() unreferenced
in registry.cpp (the DLLs call it via their own wrappers, not this TU). As a
static (internal-linkage) function that tripped MSVC C4505 under /WX; GCC's
-Wunused-function was already suppressed by a pragma in registry.cpp but that
doesn't cover MSVC. Make the header helper inline — unused inline functions
don't warn, and a header function should be inline anyway.

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

The dll/test/filesystem dependencies were pulled in via the propagated
<link>shared and built as shared libraries. That fails under sanitizers:

  * Clang UBSAN (Linux): Clang links the static UBSAN runtime, leaving the
    DSO with undefined __ubsan_*_abort symbols; Boost.Filesystem links with
    -Wl,--no-undefined, so libboost_filesystem.so fails to link. GCC links
    libubsan into the DSO and is unaffected.
  * macOS UBSAN: a shared Boost.Test built with hidden visibility gives each
    DSO its own typeinfo for lazy_ostream, tripping -fsanitize=vptr at run.

Pin these dependencies to <link>static so only our registry/method/overrider
libraries remain shared objects. Verified on Clang 18 UBSAN, Clang release,
and GCC 13 UBSAN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After linking the Boost deps statically, macOS UBSAN got past the earlier
link/lazy_ostream failures but then aborted at runtime: Clang's vptr check
fires on the open-method downcast (core.hpp optimal_cast) of objects created
inside dlopen'd libraries. All 12 failures were vptr-category ("downcast of
address ..." / "member call on address ... which does not point to an object
of type 'Dog'"). The objects really are of the expected type; the check is a
false positive because macOS/Mach-O does not deduplicate a class's typeinfo
across DSOs the way ELF does (Linux passes thanks to <local-visibility>global).

Disable just the vptr sub-check, gated on the UBSAN feature. No-op for GCC
(no vptr sanitizer) and when UBSAN is off; ASAN is unaffected. Verified on
Clang 18 UBSAN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
registry_state::st is a public static data member (the only way to expose
the DLL-shared state as a whole-class instantiation), but it is an
implementation detail that should not appear in the reference. It cannot be
made protected/private since it is read across the library via the
registry::static_ alias rather than through inheritance, so exclude it in
mrdocs.yml instead. Also drop a duplicate registry::compiler entry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The antora-cpp-reference-extension caches the downloaded MrDocs binary under a
directory keyed by tag name (.../mrdocs/<os>/develop/), so with no pinned
version a local Antora build reuses the first nightly it ever fetched and never
picks up newer develop-release builds. CI is unaffected because runners start
with an empty cache.

This helper compares the upstream develop-release asset's publish time to a
marker recording what was last synced, and clears the cache only when upstream
has moved. It is a safe no-op when offline, rate-limited, or when a concrete
version is pinned. Run it before build_antora.sh (e.g. as a VS Code task
dependency) to keep local docs on the same MrDocs CI would use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The GCC 13 stages bundled multiple C++ standards into one pipeline
(17,20,2b x 32,64 for the base job; 17,20 for the UBSAN/ASAN jobs), doing
2-3x the work of their per-standard GCC 14 siblings. After the branch added
the dynamic_loading DLL test targets, these three stages exceeded Drone's
60-minute step timeout and were SIGKILLed (exit 137) mid-compile, with no
actual build error. Every other toolset/arch passed on the same commit.

Split them one standard per stage, mirroring the GCC 14 jobs, so each stays
well under the timeout while keeping identical coverage:
  - GCC 13 32/64            -> C++17 / C++20 / C++2b
  - GCC 13 32/64 UBSAN      -> C++17 / C++20 (C++2b stage already separate)
  - GCC 13 32 ASAN          -> C++17 / C++20 (C++2b stage already separate)

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

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.62411% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.25%. Comparing base (814a8e0) to head (065fe6a).
⚠️ Report is 15 commits behind head on develop.

Files with missing lines Patch % Lines
include/boost/openmethod/initialize.hpp 89.59% 12 Missing and 6 partials ⚠️
test/test_custom_rtti_virtual_base.cpp 76.66% 14 Missing ⚠️
test/test_custom_rtti_simple.cpp 75.00% 12 Missing ⚠️
test/dynamic_loading/main.cpp 90.90% 1 Missing and 5 partials ⚠️
...de/boost/openmethod/policies/fast_perfect_hash.hpp 84.84% 2 Missing and 3 partials ⚠️
test/test_custom_rtti_deferred.cpp 89.13% 5 Missing ⚠️
test/test_custom_rtti_simple_projection.cpp 83.87% 5 Missing ⚠️
test/test_runtime_errors_duplicate_overrider.cpp 75.00% 4 Missing ⚠️
test/test_n2216_pick_any_ambiguous.cpp 88.88% 2 Missing ⚠️
test/test_runtime_errors_bad_call_type_ids.cpp 89.47% 2 Missing ⚠️
... and 6 more
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff             @@
##           develop      #75      +/-   ##
===========================================
- Coverage    94.34%   93.25%   -1.10%     
===========================================
  Files           43       82      +39     
  Lines         2916     3467     +551     
  Branches         0     1729    +1729     
===========================================
+ Hits          2751     3233     +482     
- Misses         165      186      +21     
- Partials         0       48      +48     
Files with missing lines Coverage Δ
include/boost/openmethod/default_registry.hpp 100.00% <ø> (ø)
include/boost/openmethod/detail/ostdstream.hpp 73.33% <ø> (-7.44%) ⬇️
...oost/openmethod/policies/default_error_handler.hpp 100.00% <100.00%> (ø)
...nclude/boost/openmethod/policies/stderr_output.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/policies/vptr_map.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/policies/vptr_vector.hpp 100.00% <100.00%> (ø)
include/boost/openmethod/preamble.hpp 75.92% <100.00%> (+3.19%) ⬆️
test/dynamic_loading/classes.hpp 100.00% <100.00%> (ø)
test/dynamic_loading/method.cpp 100.00% <100.00%> (ø)
test/dynamic_loading/method.hpp 100.00% <100.00%> (ø)
... and 50 more

... and 13 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dd2c40d...065fe6a. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

jll63 and others added 7 commits July 22, 2026 00:19
Fixes several issues from a max-effort review of the DLL state-sharing
branch:

- augment_methods() deduped overriders by pointer identity, so the same
  overrider registered by two state-sharing modules produced a false
  ambiguous_call; now dedups by function type + virtual-parameter types.
- augment_classes() deduped class_info by type_id alone, dropping a
  module's static_vptr under hidden ELF visibility (type_id unifies,
  static_vptr does not); now keys on (type, static_vptr).
- registry_state_type's policy-state tuple had no uniqueness check, so two
  colliding policy state types produced a cryptic "duplicate base type"
  error instead of a readable one.
- vptr_vector's policy-ordering static_assert located itself by exact type,
  silently passing for a vptr policy derived from vptr_vector.
- initialize() never reset st.initialized on entry, so a failed
  re-initialize left dispatch looking valid.
- missing_base's base/derived fields were swapped in the diagnostic.
- fast_perfect_hash's collision check treated re-inserting an identical
  type_id as a collision, which the class-dedup fix above now legitimately
  triggers under hidden visibility.
- Stale VptrFn/TypeHashFn blueprint docs, sentinel-type_id hazard in both
  custom_rtti examples (plus a defensive arity check), test bugs
  (os.clear() vs os.str(""), a missing assertion), CMake silently skipping
  dynamic_loading tests by default, three shared_libraries.adoc gaps, a
  non-inline compat header in announce.md, and a stale CLAUDE.md test
  description.
- Added 5 files that were referenced by tracked build/doc files but never
  committed.
- Removed .claude/settings.json and notes.txt from tracking (personal
  scratch files, kept on disk).

Extends test/dynamic_loading with a Cat class and shared_overrider.hpp
(the same overrider registered by two modules) plus a hidden-visibility
CMake variant, regression-testing the two dispatch fixes above; verified
each independently by reverting its fix and confirming the new test fails.

Verified: full ctest suite (112/112) under CMake+gcc and CMake+clang, plus
the complete b2 test suite, all passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…NDEBUG

The consistency-check loop added in the previous commit only uses its loop
variable inside BOOST_ASSERT, which compiles out under NDEBUG (release
builds), leaving it genuinely unused. This broke the macOS/Xcode UBSAN CI
job (-Werror,-Wunused-variable), which builds in release mode:
https://drone.cpp.al/boostorg/openmethod/253/30/2

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ders

augment_methods()'s cross-module overrider dedup (keyed on function type +
virtual-parameter types) correctly merges the same overrider duplicated
across state-sharing modules, but was also merging two genuinely different
overriders that happen to share a signature within a SINGLE module -
picking one arbitrarily instead of correctly reporting ambiguous_call, a
regression from the previous behavior (pointer-identity dedup, which never
merged same-module entries).

Fix: only compare a module's specs against specs already collected from
EARLIER modules, never against other specs from its own module. Two
overriders registered within the same module are always genuinely
distinct by construction (BOOST_OPENMETHOD_OVERRIDE keys one explicit
specialization per signature - writing it twice is a redefinition error;
method<...>::override<Fn> is keyed on the Fn non-type template argument -
two different functions are two different registrations), so same-module
entries must never be merged with each other, even when they share a
signature.

Adds test_runtime_errors_duplicate_overrider.cpp: registers two distinct
functions with an identical signature directly via method<...>::override<Fn>
(the primitive BOOST_OPENMETHOD_OVERRIDE itself expands to, which the macro
can't use twice with the same signature in one TU) and asserts the call is
reported as ambiguous_call, not silently resolved.

Verified: reproduced the bug standalone before the fix (report.ambiguous==0,
call silently returned one overrider's result), confirmed the old
pointer-identity dedup handled it correctly, confirmed the fix restores
that behavior while leaving the cross-module dedup (dynamic_loading Cat
test, including the hidden-visibility variant) intact. Full suite green
under CMake+gcc, CMake+clang, and b2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…euristic

augment_methods()'s cross-module overrider dedup previously merged two
same-signature overrider_info entries whenever they came from different
method.infos entries (an "earlier modules only" heuristic), to avoid
merging two genuinely different overriders registered within the same
module. That heuristic is replaced with a flag tied to an actual C++
invariant: BOOST_OPENMETHOD_INLINE_OVERRIDE marks its overrider_info as
inline_ = true; BOOST_OPENMETHOD_OVERRIDE never does. Only an overrider
declared `inline` can legally have an identical definition appear in more
than one translation unit/module (ODR requires it for a non-inline
function to be defined exactly once in the program), so the dedup now
merges two specs only when *both* are inline_, dropping the
module-provenance bookkeeping entirely - simpler, and tied to why merging
is actually safe rather than where the specs happened to come from.

The inline_ flag has to be a compile-time non-type template parameter
(Inline) on override_impl/override_aux, set inside override_impl's own
constructor alongside method/pf/etc, not a runtime constructor argument on
class override as first attempted: override_aux::impl is a static template
data member whose dynamic initialization is not guaranteed to happen
before class override's own constructor body runs (confirmed empirically
by instrumenting both constructors - taking impl's address doesn't force
its construction), so a runtime assignment made there could run *before*
override_impl's constructor, which then overwrites it via the implicit
default-construction of the overrider_info base. A new class
inline_override<Fn...> (using override_aux<Fn, decltype(Fn), true>)
provides the inline path; plain override<Fn...> is unchanged (Inline
always false).

BOOST_OPENMETHOD_REGISTER/BOOST_OPENMETHOD_GENSYM (both public, documented,
and used directly throughout tests and examples) are untouched;
BOOST_OPENMETHOD_INLINE_OVERRIDE just registers via
method<...>::inline_override<Fn> instead of ::override<Fn>, both routed
through the same, already fully-variadic BOOST_OPENMETHOD_REGISTER.

test/dynamic_loading/shared_overrider.hpp switches to
BOOST_OPENMETHOD_INLINE_OVERRIDE, which is also the technically-correct
ODR-legal choice for an overrider defined identically in a header included
by two modules.

Verified: full suite green under CMake+gcc, CMake+clang, and b2, including
test_runtime_errors_duplicate_overrider and all five dynamic_loading
variants (default/indirect/exereg_default/exereg_indirect/hidden_vis) -
hidden_vis specifically exercises real cross-module merging of two
independently-constructed (non-folded, hidden-visibility) overrider_info
objects, which the default-visibility variants don't, since weak-symbol
folding under RTLD_GLOBAL there means only one physical registration ever
survives regardless of the dedup logic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng test

classes.hpp declares these with internal (static) linkage since it's
included identically by multiple TUs; method.cpp and registry.cpp only
call make_dog(), never make_cat(), which MSVC's /W4 /WX build reports as
error C2220/C4505 ("unreferenced function with internal linkage has been
removed"). gcc/clang don't warn on this by default, so it only surfaced on
the Windows CI runners. Mark both [[maybe_unused]].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… review

Fixes the items the earlier review verified but cut for its 15-finding cap:

- Remove the dead has_id SFINAE detector (zero users anywhere); update the
  CLAUDE.md line that described it as load-bearing.
- Consolidate the two near-identical MSVC>19.51 has_initialize/has_finalize
  workaround blocks into one BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN_MSVC_SAFE
  macro; the maintainer #pragma message now fires once per build instead of
  twice.
- Drop registry::policy's unused Default template parameter and the
  get_policy_aux specialization it fed - undocumented, and unreachable since
  no caller ever supplied one. Rewrite has_policy_state as mp11::mp_valid.
- fast_perfect_hash: reset min_value/max_value per attempt instead of per
  pass. A failed attempt's stale range was leaking into the eventually-
  successful one, contaminating hash_range() and oversizing the vptr vector
  (a real correctness bug, not just cosmetic). Also fix search_error.buckets
  reporting double the largest table actually tried (off-by-one from the
  for-loop's post-loop ++M).
- Remove dead registry_make_dog/registry_initialize scaffolding from
  test/dynamic_loading/registry.cpp (confirmed zero call sites, including via
  dlopen).
- Replace augment_methods()'s check-then-act find()/operator[] double lookup
  with a single try_emplace.
- extensions.cpp doc snippet: move the 3 BOOST_ASSERT self-checks out of the
  rendered tag::content region (still compiled and run, just not displayed),
  and move `using namespace boost::openmethod;` inside the snippet so the
  displayed code actually compiles as shown.
- Remove the stale "gcc-8 doesn't like CTAD" comment in registry::finalize:
  gcc-8/9 can't build this library at all any more (missing full <charconv>
  support required elsewhere), so the justification was moot. The harmless
  explicit-type workaround itself is kept.
- Correct CMakeLists.txt comments claiming the _exereg_* dynamic_loading
  variants exercise lib_registry at runtime: it's built and link-configured
  for reverse linkage but never actually dlopened by main.cpp, only
  lib_method/lib_overrider are.

Deliberately left alone: the duplicated test-file preambles across
test_runtime_errors_*.cpp and the custom-RTTI tests carry genuine, easy-to-
miss deliberate divergences (guard conditions, array-index offsets, registry
configs) that a mechanical dedup would risk silently erasing; actually
wiring the _exereg_* registry lib into the runtime test (rather than just
correcting the comment) is a design decision, not a mechanical fix.

Verified: full ctest suite (113/113) under CMake+gcc and CMake+clang, plus
the complete b2 test suite, all passing.

Co-Authored-By: Claude Sonnet 5 <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