Skip to content

Commit f3b7edd

Browse files
committed
[c] support dynamic loading on Windows: test
Create a test for dynamic loading. The main program loads a shared library that exports the default registry, then a shared library that exports a method and adds it to the registry, then load a shared library that adds an overrider to the method. Check that all the static state is unique and the same for all libraries. The libraries export functions that make it possible for the main program to check that: - the registry state is the same for all libraries (use `id()`) - the policies that contain static state all use the same variables (again use `id()`). - the method is the same in all libraries (check `&fn`). On msvc, use `boost_openmethod_declspec` to import and export the variables as needed. Take inspiration from the example in `shared_libs`. Put the new test in `test/dynamic_loading`.
1 parent 13b052c commit f3b7edd

File tree

10 files changed

+364
-18
lines changed

10 files changed

+364
-18
lines changed

CLAUDE.md

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ ctest -R test_dispatch # Run specific test by name
7474
- Test files: `test/test_*.cpp` - Standard unit tests using Boost.Test
7575
- Compile-fail tests: `test/compile_fail_*.cpp` - Tests that should fail to compile
7676
- Mixed build test: `test/mix_release_debug/` - Tests mixing debug/release builds
77+
- Dynamic loading test: `test/dynamic_loading/` - Tests shared library support (requires Boost.DLL)
7778
- 21+ test files covering dispatch, policies, virtual_ptr, RTTI, errors, etc.
7879

7980
### Debug Mode Features
@@ -165,35 +166,53 @@ Tests require these C++17 features (checked by Boost.Build):
165166
**Overview**: The library supports shared library usage on Windows with proper dllexport/dllimport decoration.
166167

167168
**Key Pattern - Decoratable Static Variables**:
168-
All policy static variables use `BOOST_OPENMETHOD_DETAIL_MAKE_SYMBOL_WITH_ATTRIBUTES(name)` macro to enable DLL decoration. This generates three specializations of `global_state_<name>`:
169+
All policy static variables use `BOOST_OPENMETHOD_DETAIL_MAKE_STATICS(name)` macro (in `preamble.hpp`) to enable DLL decoration. This generates three specializations of `static_<name>`:
169170
- Default (no attributes)
170-
- `__declspec(dllexport)` when registry has dllexport attributes
171-
- `__declspec(dllimport)` when registry has dllimport attributes
172-
173-
**Affected Policies**:
174-
- `stderr_output::fn::os` - Output stream (via `global_state_os`)
175-
- `default_error_handler::fn::handler` - Error handler function (via `global_state_handler`)
176-
- `fast_perfect_hash::fn::hash_fn` - Hash factors struct (via `global_state_hash_fn`)
177-
- `vptr_map::fn::vptrs` - V-table pointer map (via `global_state_vptrs`)
171+
- `BOOST_SYMBOL_EXPORT` when registry has dllexport attributes
172+
- `BOOST_SYMBOL_IMPORT` when registry has dllimport attributes
173+
174+
The attributes are selected via `get_attributes<Guide>` which uses ADL to call `boost_openmethod_declspec(Guide)`.
175+
176+
**Affected Policies** (actual symbol names from `nm -D`):
177+
- `stderr_output::fn::os` - Output stream (via `static_os`)
178+
- `default_error_handler::fn::handler` - Error handler function (via `static_handler`)
179+
- `fast_perfect_hash::fn::hash_fn` - Hash factors struct (via `static_hash_fn`)
180+
- `vptr_map::fn::vptrs` - V-table pointer map (via `static_vptrs`)
178181
- `vptr_vector::fn::vptr_vector_vptrs` / `vptr_vector_indirect_vptrs` - V-table vectors
182+
- Registry state itself: `static_st<registry_state<Registry>>::st` - class/method/overrider lists
179183

180184
**Example Usage**:
181185
```cpp
182186
// In header shared between library and client
183187
#ifdef LIBRARY_NAME
184-
#define MY_API boost::openmethod::declspec::dllexport
188+
#define MY_API boost::openmethod::dllexport
185189
#else
186-
#define MY_API boost::openmethod::declspec::dllimport
190+
#define MY_API boost::openmethod::dllimport
187191
#endif
188192

189193
namespace boost::openmethod {
190-
MY_API boost_openmethod_attributes(default_registry_attributes);
194+
MY_API boost_openmethod_declspec(default_registry_attributes);
191195
}
192196

193197
BOOST_OPENMETHOD(my_method, (virtual_ptr<MyClass>), void, MY_API);
194198
```
195199
196-
See `doc/modules/ROOT/examples/shared_libs/` for complete examples.
200+
**Critical: Exporting the Registry State**:
201+
`static_st<registry_state<...>>::st` (the list of registered classes/methods/overriders) is only
202+
instantiated and exported from a shared library when that library compiles code that actually
203+
registers classes or methods (i.e., `BOOST_OPENMETHOD_CLASSES` or `BOOST_OPENMETHOD` macros).
204+
If a library is meant to "own" the registry state, it must include a header that triggers these
205+
registrations — it is not enough to just declare `boost_openmethod_declspec`.
206+
207+
See `doc/modules/ROOT/examples/shared_libs/` and `test/dynamic_loading/` for complete examples.
208+
209+
**Dynamic Loading Test** (`test/dynamic_loading/`):
210+
Verifies shared state across libraries using `id()` functions on all policy statics:
211+
- `classes.hpp` — class definitions + `BOOST_OPENMETHOD_CLASSES` (included by lib_registry with dllexport to force `st` export)
212+
- `lib_registry.cpp` — compiled with `REGISTRY_API=dllexport`, exports all registry statics
213+
- `lib_method.cpp` — compiled with `METHOD_API=dllexport`, imports registry from lib_registry
214+
- `lib_overrider.cpp` — dynamically loaded at runtime, adds a Dog overrider
215+
- `main.cpp` — loads lib_overrider, checks all `id()` values match, calls `initialize()`, tests dispatch
197216
198217
### Custom RTTI
199218
When `<typeinfo>` is unavailable or insufficient, use static_rtti or implement custom RTTI. See `doc/modules/ROOT/examples/custom_rtti/` and policies in `include/boost/openmethod/policies/`.
@@ -269,10 +288,10 @@ The v-table pointer enables O(1) method dispatch.
269288
270289
When adding static variables to policies:
271290
272-
1. **Declare the variable storage** in `detail` namespace using `BOOST_OPENMETHOD_DETAIL_MAKE_SYMBOL_WITH_ATTRIBUTES(variable_name)`
273-
2. **Use type alias** in policy's `fn<Registry>` class: `using var_storage = detail::global_state_variable_name<Type, Registry>`
291+
1. **Declare the variable storage** in `detail` namespace using `BOOST_OPENMETHOD_DETAIL_MAKE_STATICS(variable_name)` (defined in `preamble.hpp`)
292+
2. **Use type alias** in policy's `fn<Registry>` class: `using var_storage = detail::static_variable_name<Type, Registry>`
274293
3. **Access via storage**: `var_storage::variable_name` instead of direct static member
275-
4. **Define specialization** outside class: `template<class Registry> Type detail::global_state_variable_name<Type, Registry>::variable_name;`
294+
4. **Definition is generated** by the macro: `template<class Registry> Type detail::static_variable_name<Type, Registry, Enable>::variable_name;`
276295
277296
This pattern ensures static variables can be properly decorated with dllexport/dllimport for shared library usage.
278297
@@ -303,12 +322,12 @@ struct hash_fn {
303322
return (mult * reinterpret_cast<uintptr>(type)) >> shift;
304323
}
305324
};
306-
BOOST_OPENMETHOD_DETAIL_MAKE_SYMBOL_WITH_ATTRIBUTES(hash_fn);
325+
BOOST_OPENMETHOD_DETAIL_MAKE_STATICS(hash_fn); // generates static_hash_fn<Type, Registry, Enable>
307326

308327
// In policy
309328
template<class Registry>
310329
class fn {
311-
using factors_storage = detail::global_state_hash_fn<detail::hash_fn, Registry>;
330+
using factors_storage = detail::static_hash_fn<detail::hash_fn, Registry>;
312331
public:
313332
static auto hash(type_id type) -> std::size_t {
314333
return factors_storage::hash_fn(type); // Use via storage

test/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,7 @@ openmethod_compile_fail_test(
102102
compile_fail_virtual_parameter_private_base_core "must be an unambiguous accessible base")
103103
openmethod_compile_fail_test(
104104
compile_fail_repeated_inheritance "repeated inheritance")
105+
106+
if (TARGET Boost::dll)
107+
add_subdirectory(dynamic_loading)
108+
endif()
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright (c) 2018-2025 Jean-Louis Leroy
2+
# Distributed under the Boost Software License, Version 1.0.
3+
# See accompanying file LICENSE_1_0.txt
4+
# or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
# lib_registry: exports the default registry's static policy state
7+
add_library(dl_test_registry SHARED EXCLUDE_FROM_ALL lib_registry.cpp)
8+
target_compile_definitions(
9+
dl_test_registry PRIVATE BOOST_OPENMETHOD_DYNAMIC_LOADING_REGISTRY_EXPORTS)
10+
target_link_libraries(dl_test_registry PUBLIC Boost::openmethod)
11+
12+
# lib_method: exports the speak method; imports registry state from lib_registry
13+
add_library(dl_test_method SHARED EXCLUDE_FROM_ALL lib_method.cpp)
14+
target_compile_definitions(
15+
dl_test_method PRIVATE BOOST_OPENMETHOD_DYNAMIC_LOADING_METHOD_EXPORTS)
16+
target_link_libraries(dl_test_method PUBLIC Boost::openmethod dl_test_registry)
17+
18+
# lib_overrider: adds a Dog overrider; imported at runtime via Boost.DLL
19+
add_library(dl_test_overrider SHARED EXCLUDE_FROM_ALL lib_overrider.cpp)
20+
target_link_libraries(dl_test_overrider PRIVATE Boost::openmethod dl_test_method)
21+
22+
# Test executable: links against lib_method (and lib_registry transitively);
23+
# dynamically loads lib_overrider at runtime.
24+
add_executable(
25+
boost_openmethod-test_dynamic_loading EXCLUDE_FROM_ALL main.cpp)
26+
target_link_libraries(
27+
boost_openmethod-test_dynamic_loading
28+
PRIVATE Boost::openmethod Boost::unit_test_framework Boost::dll
29+
dl_test_method)
30+
31+
# Put all outputs in the same directory so Boost.DLL can locate the shared
32+
# libraries relative to the test executable at runtime.
33+
foreach(
34+
target dl_test_registry dl_test_method dl_test_overrider
35+
boost_openmethod-test_dynamic_loading)
36+
set_target_properties(
37+
${target}
38+
PROPERTIES
39+
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
40+
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}"
41+
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
42+
endforeach()
43+
44+
add_test(
45+
NAME boost_openmethod-test_dynamic_loading
46+
COMMAND boost_openmethod-test_dynamic_loading)
47+
add_dependencies(
48+
tests boost_openmethod-test_dynamic_loading dl_test_overrider)

test/dynamic_loading/classes.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) 2018-2025 Jean-Louis Leroy
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// See accompanying file LICENSE_1_0.txt
4+
// or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#pragma once
7+
8+
#include "registry.hpp"
9+
10+
#include <boost/openmethod.hpp>
11+
12+
struct Animal {
13+
virtual ~Animal() = default;
14+
};
15+
16+
struct Dog : Animal {};
17+
18+
BOOST_OPENMETHOD_CLASSES(Animal, Dog);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright (c) 2018-2025 Jean-Louis Leroy
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// See accompanying file LICENSE_1_0.txt
4+
// or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
// BOOST_OPENMETHOD_DYNAMIC_LOADING_METHOD_EXPORTS is set via CMake compile
7+
// definition, making METHOD_API expand to dllexport in method.hpp.
8+
#include "method.hpp"
9+
10+
using namespace boost::openmethod;
11+
12+
using Method = BOOST_OPENMETHOD_TYPE(speak, (virtual_ptr<Animal>), std::string);
13+
14+
#ifdef _WIN32
15+
#define DL_EXPORT __declspec(dllexport)
16+
#else
17+
#define DL_EXPORT
18+
#endif
19+
20+
extern "C" {
21+
22+
DL_EXPORT auto dl_method_get_vptrs_id() -> const void* {
23+
return default_registry::vptr::id();
24+
}
25+
26+
DL_EXPORT auto dl_method_get_error_handler_id() -> const void* {
27+
return default_registry::error_handler::id();
28+
}
29+
30+
DL_EXPORT auto dl_method_get_output_id() -> const void* {
31+
return default_registry::output::id();
32+
}
33+
34+
DL_EXPORT auto dl_method_get_type_hash_id() -> const void* {
35+
return default_registry::policy<policies::type_hash>::id();
36+
}
37+
38+
DL_EXPORT auto dl_method_get_method_fn() -> const void* {
39+
return static_cast<const void*>(&Method::fn);
40+
}
41+
42+
} // extern "C"
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2018-2025 Jean-Louis Leroy
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// See accompanying file LICENSE_1_0.txt
4+
// or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#include "method.hpp"
7+
8+
using namespace boost::openmethod;
9+
10+
BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr<Dog>), std::string) {
11+
return "woof";
12+
}
13+
14+
using Method = BOOST_OPENMETHOD_TYPE(speak, (virtual_ptr<Animal>), std::string);
15+
16+
#ifdef _WIN32
17+
#define DL_EXPORT __declspec(dllexport)
18+
#else
19+
#define DL_EXPORT
20+
#endif
21+
22+
extern "C" {
23+
24+
DL_EXPORT auto dl_overrider_get_vptrs_id() -> const void* {
25+
return default_registry::vptr::id();
26+
}
27+
28+
DL_EXPORT auto dl_overrider_get_error_handler_id() -> const void* {
29+
return default_registry::error_handler::id();
30+
}
31+
32+
DL_EXPORT auto dl_overrider_get_output_id() -> const void* {
33+
return default_registry::output::id();
34+
}
35+
36+
DL_EXPORT auto dl_overrider_get_type_hash_id() -> const void* {
37+
return default_registry::policy<policies::type_hash>::id();
38+
}
39+
40+
DL_EXPORT auto dl_overrider_get_method_fn() -> const void* {
41+
return static_cast<const void*>(&Method::fn);
42+
}
43+
44+
} // extern "C"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) 2018-2025 Jean-Louis Leroy
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// See accompanying file LICENSE_1_0.txt
4+
// or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
// BOOST_OPENMETHOD_DYNAMIC_LOADING_REGISTRY_EXPORTS is set via CMake compile
7+
// definition, making REGISTRY_API expand to dllexport in registry.hpp (and
8+
// transitively in classes.hpp). Including classes.hpp forces
9+
// static_st<registry_state<...>>::st to be instantiated and exported from
10+
// this shared library, so other libraries can import it.
11+
#include "classes.hpp"
12+
13+
using namespace boost::openmethod;
14+
15+
#ifdef _WIN32
16+
#define DL_EXPORT __declspec(dllexport)
17+
#else
18+
#define DL_EXPORT
19+
#endif
20+
21+
extern "C" {
22+
23+
DL_EXPORT auto dl_registry_get_vptrs_id() -> const void* {
24+
return default_registry::vptr::id();
25+
}
26+
27+
DL_EXPORT auto dl_registry_get_error_handler_id() -> const void* {
28+
return default_registry::error_handler::id();
29+
}
30+
31+
DL_EXPORT auto dl_registry_get_output_id() -> const void* {
32+
return default_registry::output::id();
33+
}
34+
35+
DL_EXPORT auto dl_registry_get_type_hash_id() -> const void* {
36+
return default_registry::policy<policies::type_hash>::id();
37+
}
38+
39+
} // extern "C"

test/dynamic_loading/main.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) 2018-2025 Jean-Louis Leroy
2+
// Distributed under the Boost Software License, Version 1.0.
3+
// See accompanying file LICENSE_1_0.txt
4+
// or copy at http://www.boost.org/LICENSE_1_0.txt)
5+
6+
#define BOOST_TEST_MODULE dynamic_loading
7+
#include <boost/test/unit_test.hpp>
8+
9+
#include "method.hpp"
10+
11+
#include <boost/openmethod/initialize.hpp>
12+
#include <boost/dll/shared_library.hpp>
13+
#include <boost/dll/runtime_symbol_info.hpp>
14+
15+
using namespace boost::openmethod;
16+
17+
using Method = BOOST_OPENMETHOD_TYPE(speak, (virtual_ptr<Animal>), std::string);
18+
19+
// C functions exported by lib_registry (linked against the test executable)
20+
extern "C" {
21+
const void* dl_registry_get_vptrs_id();
22+
const void* dl_registry_get_error_handler_id();
23+
const void* dl_registry_get_output_id();
24+
const void* dl_registry_get_type_hash_id();
25+
}
26+
27+
// C functions exported by lib_method (linked against the test executable)
28+
extern "C" {
29+
const void* dl_method_get_vptrs_id();
30+
const void* dl_method_get_error_handler_id();
31+
const void* dl_method_get_output_id();
32+
const void* dl_method_get_type_hash_id();
33+
const void* dl_method_get_method_fn();
34+
}
35+
36+
BOOST_AUTO_TEST_CASE(test_shared_state) {
37+
namespace dll = boost::dll;
38+
39+
// Dynamically load lib_overrider
40+
dll::shared_library overrider_lib(
41+
dll::program_location().parent_path() / "dl_test_overrider",
42+
dll::load_mode::append_decorations);
43+
44+
auto overrider_get_vptrs_id =
45+
overrider_lib.get<const void*()>("dl_overrider_get_vptrs_id");
46+
auto overrider_get_error_handler_id =
47+
overrider_lib.get<const void*()>("dl_overrider_get_error_handler_id");
48+
auto overrider_get_output_id =
49+
overrider_lib.get<const void*()>("dl_overrider_get_output_id");
50+
auto overrider_get_type_hash_id =
51+
overrider_lib.get<const void*()>("dl_overrider_get_type_hash_id");
52+
auto overrider_get_method_fn =
53+
overrider_lib.get<const void*()>("dl_overrider_get_method_fn");
54+
55+
// All libraries must share the same vptr storage
56+
const void* vptrs_id = default_registry::vptr::id();
57+
BOOST_TEST(dl_registry_get_vptrs_id() == vptrs_id);
58+
BOOST_TEST(dl_method_get_vptrs_id() == vptrs_id);
59+
BOOST_TEST(overrider_get_vptrs_id() == vptrs_id);
60+
61+
// All libraries must share the same error handler storage
62+
const void* error_handler_id = default_registry::error_handler::id();
63+
BOOST_TEST(dl_registry_get_error_handler_id() == error_handler_id);
64+
BOOST_TEST(dl_method_get_error_handler_id() == error_handler_id);
65+
BOOST_TEST(overrider_get_error_handler_id() == error_handler_id);
66+
67+
// All libraries must share the same output storage
68+
const void* output_id = default_registry::output::id();
69+
BOOST_TEST(dl_registry_get_output_id() == output_id);
70+
BOOST_TEST(dl_method_get_output_id() == output_id);
71+
BOOST_TEST(overrider_get_output_id() == output_id);
72+
73+
// All libraries must share the same type hash storage
74+
const void* type_hash_id =
75+
default_registry::policy<policies::type_hash>::id();
76+
BOOST_TEST(dl_registry_get_type_hash_id() == type_hash_id);
77+
BOOST_TEST(dl_method_get_type_hash_id() == type_hash_id);
78+
BOOST_TEST(overrider_get_type_hash_id() == type_hash_id);
79+
80+
// All libraries must see the same method object
81+
const void* method_fn = static_cast<const void*>(&Method::fn);
82+
BOOST_TEST(dl_method_get_method_fn() == method_fn);
83+
BOOST_TEST(overrider_get_method_fn() == method_fn);
84+
85+
// Build dispatch tables (including the Dog overrider from the loaded lib)
86+
boost::openmethod::initialize();
87+
88+
// Verify dispatch
89+
Dog dog;
90+
BOOST_TEST(speak(dog) == "woof");
91+
}

0 commit comments

Comments
 (0)