Skip to content

support DLLs on Windows#75

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

support DLLs on Windows#75
jll63 wants to merge 95 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 14 commits May 24, 2026 09:50
Replace declspec(import/export) approach with method consolidation:
- Remove detail::static_fn infrastructure
- Revert fn to simple static method member (not via MAKE_STATICS)
- In augment_methods(), group methods by type_id and collect
  overriders from all module copies into canonical method
- Build dispatch tables once per unique method, not once per copy

This avoids intrusive list manipulation during initialization while
enabling correct dispatch table construction with shared method state
across DLL boundaries.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Resolve deferred type IDs before consolidation (deferred_static_rtti)
- Update test_custom_rtti to assign unique IDs to non-polymorphic types
  via non_polymorphic_static_type<T>() function-local counter, so method
  tag classes don't collapse to a single sentinel and trigger spurious
  ambiguous dispatch
- Remove get_fn test from dynamic_loading: each module now has its own
  fn instance, so address equality no longer holds (the state is
  synchronized via consolidation at initialize time instead)

All 77 tests pass.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add `meet` multi-method to test_dynamic_loading so the test exercises
  multiple methods and non-zero slots/strides — previously the test
  accidentally passed because the lone `speak` method got slot 0
  (matching zero-initialized arrays in non-local module fns)
- Rename `canonical_methods` → `local_methods` in augment_methods: the
  selected method_info isn't canonical in any global sense, it's just
  the one that happens to live in the module performing initialize()
- After dispatch table build, copy slots_strides values from each
  local method_info to all other module copies. Each module's `fn`
  has its own slots_strides[] array and dispatch reads it directly,
  so every copy must hold the same values.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Iterating methods directly and skipping the self pointer is clearer than
building a side map keyed by type_index.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Revert from signed int back to std::size_t. To distinguish polymorphic
from non-polymorphic types, set the high bit (1 << (sizeof(size_t)*8-1))
in the non_polymorphic_static_type counter values. Polymorphic Animal/
Dog/Cat keep small positive values 1/2/3; method tag classes get values
like 0x800...001, 0x800...002 — comfortably outside the 1..3 range used
by type_name. Avoids the size mismatch warnings (C4312) that b2's W4/WX
flags treat as errors on the int->const-void* reinterpret_cast path.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Policy state moved into registry_state, so stderr_output::os is now a deprecated alias and stream() returns the registry-backed stream. The library's own diagnostics still wrote to os, which broke warnings-as-errors builds (e.g. b2 with /WX). Route them through stream() and give the test's capture_output policy a stream() accessor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
boost::dll resolves the relative library name against the working directory. Without WORKING_DIRECTORY, ctest ran the test from the binary tree where a stale boost_openmethod-shared.dll could be picked up, failing at load with ERROR_PROC_NOT_FOUND. Point it at $<TARGET_FILE_DIR:boost_openmethod-dynamic>, where the freshly-built DLL sits co-located with the exe.

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

cppalliance-bot commented Jun 13, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://75.openmethod.prtest3.cppalliance.org/libs/openmethod/doc/html/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-07-03 15:51:03 UTC

@jll63 jll63 force-pushed the feature/windll3 branch from e9e5c26 to 01127a4 Compare June 13, 2026 21:28
jll63 and others added 10 commits June 14, 2026 10:39
registry_state::policy<P>() uses the type-indexed std::get<T>(tuple),
declared in <tuple>. Newer toolchains pulled the header in transitively,
but gcc-12's libstdc++ did not, so only the std::pair overloads of
std::get were visible and the call failed to compile. Include <tuple>
explicitly in the headers that use std::tuple/std::get.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main.cpp loads and calls method_get_ids/overrider_get_ids through
policy_ids_fn = const void**(), but the exported functions returned
const void*. The pointer reinterpretation is harmless at the ABI level,
so the test passes everywhere except the clang UBSan job, where
-fsanitize=function traps the call through the mismatched function-pointer
type and aborts. Declare the exports to return const void** to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a second axis to the dynamic_loading test variants: which module owns
and exports the single shared registry-state symbol. Selected at compile
time via the REGISTRY_IN_EXE macro -- registry.cpp exports the state unless
REGISTRY_IN_EXE is set, in which case main.cpp (the executable) exports it
and the shared libraries import it.

CMake builds all four variants ({dll,exe}-owned x {default,indirect}); the
exe-owned ones link the libraries against the executable's import library
(ENABLE_EXPORTS). b2 builds only the two dll-owned variants: it does not
expose an executable's import library to dependent DLLs, so the reverse
linkage cannot be expressed on Windows.

Also rename the default-registry variant suffix from "" to "_default".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…DR violation with dynamic loading

When a project uses <visibility>hidden (which adds -fvisibility-inlines-hidden),
template static variable initializers like odr_check::inc<R> get hidden symbol
visibility. When multiple shared libraries are loaded via RTLD_GLOBAL on ELF
systems (e.g. s390x), hidden symbols are not deduplicated by the dynamic linker.
Each DSO initializes its own copy of inc<R>, each time incrementing the shared
count variable. When count > 1, initialize() falsely fires the ODR violation check.

Fix: add BOOST_SYMBOL_VISIBLE to odr_check struct, which expands to
__attribute__((__visibility__("default"))) on GCC/Clang (no-op on MSVC/Windows).
This overrides -fvisibility-inlines-hidden for all struct members including the
inc<R> template static, ensuring proper RTLD_GLOBAL symbol deduplication.

Also add the required #include <boost/config.hpp> for BOOST_SYMBOL_VISIBLE.
Sharing a registry's state across DLL boundaries no longer relies on the
dllvar policy category (policies::dllexport/dllimport markers) and the
SFINAE-specialized static_st keyed on a per-TU-varying base class. Instead,
the owning module compiles a dllexport-ed explicit instantiation definition
of detail::static_st<Registry::registry_type> in exactly one translation
unit, and clients compile a dllimport-ed explicit instantiation declaration
(extern template), referencing the owner's symbol.

- BOOST_OPENMETHOD_{EXPORT,IMPORT}_DEFAULT_REGISTRY keep their contract and
  now emit the instantiation for default_registry.
- indirect_registry gets its own pair,
  BOOST_OPENMETHOD_{EXPORT,IMPORT}_INDIRECT_REGISTRY.
- Custom registries: no library-provided macro; users write the extern
  template declaration / explicit instantiation themselves.
- Remove the now-unneeded clang -Wundefined-var-template pragma machinery.
- test/dynamic_loading: registry.hpp selects the macro pair matching the
  registry under test; drop the dllvar static_asserts; remove t.cpp scratch.
- Update shared_libraries.adoc (still documented the ancient
  boost_openmethod_declspec ADL hook) and CLAUDE.md.

Verified: all 80 CMake tests pass under MSVC (including the four
dynamic_loading variants); b2 dynamic_loading variants pass under Cygwin
gcc-13.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test runs the exe from its own directory and loads the library by
relative name. On Windows a DLL is a runtime output and already lands next
to the exe, but on other platforms the shared library stayed in the
subdirectory build tree, so the test failed with "cannot open shared object
file". Set the library's LIBRARY_OUTPUT_DIRECTORY to the exe's directory.

Verified: 80/80 tests pass on WSL Ubuntu 24.04 with gcc 13.3 and clang 18.1;
the test still passes under MSVC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…istry_state_type

Rename the state-holding struct detail::registry_state to
detail::registry_state_type (it keeps the class/method lists, dispatch data,
and the policies tuple), and promote the one-member wrapper detail::static_st
to the public boost::openmethod::registry_state, whose only member `st` holds
a registry_state_type instance.

The wrapper must stay a separate, function-free, single-member class: MSVC
honors dllexport/dllimport only on a whole-class explicit instantiation, not
on a variable template (clients silently get a private copy) nor on a
static-data-member instantiation (error C2720); and dllexporting
registry_state_type directly would also decorate its member functions and the
policies' nested state types, which MSVC rejects (error C2513). A doc comment
on registry_state now records this.

The private `static_` alias is repointed at registry_state<registry>, so
core.hpp and initialize.hpp (which use static_::st) are unchanged, and the
injected-class-name normalization to the base registry is preserved. The four
explicit instantiations in default_registry.hpp name registry_state<...>.
CLAUDE.md and shared_libraries.adoc updated to the new names.

Verified 80/80 tests on MSVC (VS 2026), gcc 13.3, and clang 18.1, including
all four dynamic_loading cross-module variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The b2/CI build (/W4 /WX) promoted C4251 to error C2220 at the
registry_state<R>::st static member: the dll-exported wrapper holds a member
of type registry_state_type<R>, which intentionally has no dll-interface (it
contains std::vector/std::tuple and cannot be exported without cascading into
the policies' nested state types). The warning is benign for a static member,
which is not part of object layout.

Add 4251 to the existing _MSC_VER warning-disable block in preamble.hpp (a
single mechanism that covers the warning's emit site for both b2 and CMake),
and remove the now-redundant /wd4251 from the CMake test flags. gcc/clang are
unaffected (pragma is _MSC_VER-guarded).

Verified with cl /W4 /WX on a minimal export TU (no C4251) and with b2 msvc
test/dynamic_loading (default + indirect variants pass); full CMake suite
80/80.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jll63 jll63 requested a review from Copilot July 3, 2026 16:37
@jll63 jll63 linked an issue Jul 3, 2026 that may be closed by this pull request

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 Windows dllimport/dllexport via 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 of registry_state<registry> and does not have dispatch_data / initialized members. This looks like a compile-time error and should be updated to clear/reset the fields on the shared state object (i.e., the st member that actually owns dispatch_data and initialized).
    include/boost/openmethod/policies/vptr_vector.hpp:1
  • vptr_vector::initialize() now depends on type_hash::hash_range() having been populated already. With the new generic policy initialization that iterates Registry::policy_list in-order, this introduces an order dependency: if a registry lists vptr_vector before the type_hash policy, hash_range() may return default/uninitialized values, leading to incorrect sizing and potential out-of-bounds access later in dynamic_vptr(). A robust fix is to make vptr_vector compute its required range without relying on prior initialization order (e.g., call a well-defined type_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 shared state::os. Any remaining internal or user code that still writes to Registry::output::os will silently bypass the shared-state stream (and on Windows may reintroduce per-module divergence). Consider removing os entirely (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 references mp11, detail, and std::size_t without including the needed headers or declaring the mp11 alias; 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 similarly t.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.

Comment on lines 27 to +32
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)
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