support DLLs on Windows#75
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds Windows DLL/shared-library support by centralizing registry/policy mutable state into a single exported/imported registry-state symbol, and introduces build + test coverage to validate cross-module state sharing and dispatch.
Changes:
- Refactors registry and policy state to live under
registry_state<Registry>::st, enabling Windowsdllimport/dllexportvia explicit template instantiation. - Updates policies and initialization flow to use per-registry shared state, and consolidates method copies across modules during
initialize(). - Adds dynamic-loading tests and updates CMake/b2 plumbing and docs/examples for Windows DLL scenarios.
Reviewed changes
Copilot reviewed 44 out of 203 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/test_util.hpp | Refactors test registries to use counter-based uniqueness. |
| test/test_runtime_errors.cpp | Adjusts output capture to use a stream accessor. |
| test/test_policies.cpp | Updates registry tests and adds static-assert coverage for policy initialization detection. |
| test/test_dispatch.cpp | Removes vptr_vector finalize assertions tied to old global statics. |
| test/test_custom_rtti.cpp | Updates custom RTTI to use stable per-type storage for non-polymorphic types and adjusts IDs. |
| test/test_core.cpp | Updates registries to the new counter-based test_registry_ helper. |
| test/test_compiler.cpp | Updates type-info access to new compiler class representation. |
| test/test_class_registration.cpp | Adds test ensuring registries have distinct state/IDs. |
| test/dynamic_loading/registry.hpp | Adds registry-selection + import/export macro wiring and shared-state ID introspection helper. |
| test/dynamic_loading/registry.cpp | Adds registry “owner” module exporting registry state and C symbols. |
| test/dynamic_loading/overrider.cpp | Adds overrider shared library exporting entry points for tests. |
| test/dynamic_loading/method.hpp | Declares open methods used across modules for dynamic-loading tests. |
| test/dynamic_loading/method.cpp | Adds base method shared library with overriders and exported entry points. |
| test/dynamic_loading/main.cpp | Adds Boost.DLL-based test validating shared registry state and cross-module dispatch. |
| test/dynamic_loading/get_ids.hpp | Adds generic get_ids helper (currently incomplete). |
| test/dynamic_loading/classes.hpp | Adds shared class hierarchy + factory for dynamic-loading tests. |
| test/dynamic_loading/Jamfile | Adds b2 build graph mirroring CMake dynamic-loading tests. |
| test/dynamic_loading/CMakeLists.txt | Adds CMake variants for dll-/exe-owned registry state and direct/indirect registry. |
| test/Jamfile | Adds dynamic_loading test subproject to b2. |
| test/CMakeLists.txt | Adds boost_openmethod_add_test() helper and conditionally enables dynamic_loading tests. |
| t.txt | Adds a standalone text file (appears unrelated). |
| notes.txt | Adds local build notes/log output (appears unrelated). |
| include/boost/openmethod/preamble.hpp | Introduces registry_state + registry/policy state plumbing and updates registry internals for DLL sharing. |
| include/boost/openmethod/policies/vptr_vector.hpp | Moves vptr storage into per-registry policy state. |
| include/boost/openmethod/policies/vptr_map.hpp | Moves vptr map into per-registry policy state. |
| include/boost/openmethod/policies/stderr_output.hpp | Moves stderr output stream into per-registry policy state and adds stream() accessor. |
| include/boost/openmethod/policies/fast_perfect_hash.hpp | Moves hash state into per-registry policy state; adds hash_range(). |
| include/boost/openmethod/policies/default_error_handler.hpp | Moves default handler state into per-registry policy state; routes output via stream(). |
| include/boost/openmethod/macros.hpp | Reorders va_args partial specialization to avoid ambiguity. |
| include/boost/openmethod/initialize.hpp | Adds policy init helpers, consolidates methods across modules, propagates slots/strides to copies, and routes policy init through the new state model. |
| include/boost/openmethod/default_registry.hpp | Adds Windows explicit instantiation import/export hooks for default/indirect registries. |
| include/boost/openmethod/core.hpp | Routes class/method catalogs through shared registry state and adjusts method bookkeeping for consolidation. |
| doc/modules/ROOT/pages/shared_libraries.adoc | Updates docs describing Windows DLL import/export mechanism and build setup. |
| doc/modules/ROOT/examples/shared_libs/extensions.cpp | Updates shared-library example to use next correctly and export via BOOST_SYMBOL_EXPORT. |
| doc/modules/ROOT/examples/shared_libs/dynamic_main.cpp | Updates loading example to use rtld_global/append_decorations and improved initialization. |
| doc/modules/ROOT/examples/shared_libs/animals.hpp | Adds Windows registry-state import/export macro setup before including OpenMethod headers. |
| doc/modules/ROOT/examples/shared_libs/CMakeLists.txt | Updates example build to support Windows DLL usage (including reverse linkage). |
| doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp | Fixes integer-to-pointer casts via std::uintptr_t for portability. |
| doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp | Fixes integer-to-pointer casts via std::uintptr_t for portability. |
| doc/modules/ROOT/examples/CMakeLists.txt | Always builds shared_libs examples. |
| announce.md | Adds announcement document (unrelated to DLL mechanics). |
| CLAUDE.md | Adds repository guidance file (tooling/documentation). |
| .github/workflows/ci.yml | Updates excluded compilers list. |
| .claude/settings.json | Adds tool-specific settings. |
Comments suppressed due to low confidence (5)
include/boost/openmethod/initialize.hpp:1
static_is an alias ofregistry_state<registry>and does not havedispatch_data/initializedmembers. This looks like a compile-time error and should be updated to clear/reset the fields on the shared state object (i.e., thestmember that actually ownsdispatch_dataandinitialized).
include/boost/openmethod/policies/vptr_vector.hpp:1vptr_vector::initialize()now depends ontype_hash::hash_range()having been populated already. With the new generic policy initialization that iteratesRegistry::policy_listin-order, this introduces an order dependency: if a registry listsvptr_vectorbefore thetype_hashpolicy,hash_range()may return default/uninitialized values, leading to incorrect sizing and potential out-of-bounds access later indynamic_vptr(). A robust fix is to makevptr_vectorcompute its required range without relying on prior initialization order (e.g., call a well-definedtype_hash::initialize(...)/range computation itself, or enforce/init-order constraints in the policy initialization framework).
include/boost/openmethod/policies/stderr_output.hpp:1- The deprecated
inline static detail::ostderr os;is a distinct object from the per-registry sharedstate::os. Any remaining internal or user code that still writes toRegistry::output::oswill silently bypass the shared-state stream (and on Windows may reintroduce per-module divergence). Consider removingosentirely (breaking change) or replacing it with an API that aliases the shared stream (e.g., a function-based accessor or a proxy) so legacy call sites still hit the shared state.
test/dynamic_loading/get_ids.hpp:1 - This header is missing its closing
#endif, which will break compilation if included. Additionally, it referencesmp11,detail, andstd::size_twithout including the needed headers or declaring themp11alias; either add the missing includes/aliases (and the#endif) or remove this file if it’s not intended to be part of the build.
notes.txt:1 - The PR is about supporting DLLs on Windows, but
notes.txt(and similarlyt.txt) looks like local scratch/log output rather than project documentation or a test artifact. If these were committed unintentionally, they should be removed from the PR; if they are meant to be kept, consider moving them into an appropriate docs/troubleshooting location and trimming to the minimal actionable guidance.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| add_executable(boost_openmethod-dynamic dynamic_main.cpp) | ||
| set_target_properties(boost_openmethod-dynamic PROPERTIES | ||
| set_target_properties(boost_openmethod-dynamic PROPERTIES ENABLE_EXPORTS ON) | ||
| target_link_libraries(boost_openmethod-dynamic Boost::openmethod Boost::dll) | ||
|
|
||
| add_library(boost_openmethod-shared SHARED extensions.cpp) | ||
| target_link_libraries(boost_openmethod-shared PRIVATE Boost::openmethod boost_openmethod-dynamic) |
Each test now has its own translation unit and can use the default registry directly, instead of the per-test test_registry_<__COUNTER__> isolation hack needed when multiple tests shared one TU. The animals fixture (Animal/Property/Dog/Cat) moves to test_util.hpp for reuse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Non-template tests now use the default registry directly instead of the per-test test_registry_<__COUNTER__>/GENSYM isolation needed when sharing one TU. The three template tests keep their parameterization over direct- and indirect-vptr registries, each moved to its own file; their shared Player/Warrior/Bear/Object/Axe fixtures and policy_types machinery move to test_util.hpp for reuse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every test needs a custom registry (output capture or throw_error_handler), so instead of the old style of passing an explicit Registry argument everywhere, each file sets its custom registry via BOOST_OPENMETHOD_DEFAULT_REGISTRY before including core.hpp, matching the pattern already used in test_static_rtti.cpp and test_inplace_vptr.cpp. The shared capture_output/capture_errors helpers move to a new test_capture_errors.hpp, kept dependent only on preamble.hpp so it can be included before the registry override. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
No description provided.