diff --git a/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp b/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp index 7965e68d..2b84cf5e 100644 --- a/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp +++ b/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp @@ -69,11 +69,9 @@ struct custom_rtti : boost::openmethod::policies::rtti { template static auto dynamic_type(const T& obj) { - if constexpr (is_polymorphic) { - return reinterpret_cast(obj.type); - } else { - return static_type(); - } + static_assert(is_polymorphic); + + return reinterpret_cast(obj.type); } }; }; diff --git a/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp b/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp index 72d100b0..b5c7d560 100644 --- a/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp +++ b/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp @@ -65,11 +65,9 @@ struct custom_rtti : boost::openmethod::policies::deferred_static_rtti { template static auto dynamic_type(const T& obj) { - if constexpr (is_polymorphic) { - return reinterpret_cast(obj.type); - } else { - return static_type(); - } + static_assert(is_polymorphic); + + return reinterpret_cast(obj.type); } }; }; diff --git a/doc/modules/ROOT/pages/custom_rtti.adoc b/doc/modules/ROOT/pages/custom_rtti.adoc index cfd0640d..30e5bdb0 100644 --- a/doc/modules/ROOT/pages/custom_rtti.adoc +++ b/doc/modules/ROOT/pages/custom_rtti.adoc @@ -46,18 +46,25 @@ struct std_rtti : rtti { }; ---- -* `is_polymorphic` is used to check if a class is polymorphic. +* `is_polymorphic` tells whether `dynamic_type` can read the class of an + object of a class, which is what makes the class usable as `virtual_` + and lets a `virtual_ptr` be built from a reference or a pointer. It need not + agree with C{plus}{plus}: see the example below. This template is required. * `static_type` is called during class registration, by - `virtual_ptr`{empty}s "final" constructs. - It is also called to set `bad_call::method`. + `virtual_ptr`{empty}s "final" constructs, and to name a class in an error + or a trace. It is called for every type the library has to name, not only + registered classes: a method, and each argument of a cpp:bad_call[], + including the arguments that are not classes at all, such as an `int` or a + `std::ostream&`. This function is required. -* `dynamic_type` is used to read the dynamic type of a virtual argument. - If only the `virtual_ptr` "final" constructs are used, or if - cpp:boost_openmethod_vptr[] is provided for all the classes in the registry, - this function can be omitted. +* `dynamic_type` reads the class of a virtual argument. The library calls it + for a class that `is_polymorphic` deems polymorphic, and for no other, so a + policy that deems no class polymorphic - like + cpp:policies::static_rtti[] - may omit it. Providing `is_polymorphic` as + `true` without it is an error, and is diagnosed as one. * `type_name` writes a representation of a type to a `LightweightOutputStream`. @@ -65,10 +72,15 @@ struct std_rtti : rtti { This function is optional; if it is not provided, _type_ is rendered as "type_id(_type_)". -* `type_index` returns an object that _uniquely_ identifies a class. +* `type_index` returns an object that identifies a class across modules. Some forms of RTTI (most notably, C{plus}{plus}'s `typeid` operator) do not guarantee that the type information object for a class is unique within - the same program. + the same program: there is typically one per module. The key must compare + equal for all of them, and differ for two different classes - including + two classes with the same name in different translation units, such as + classes defined in anonymous namespaces, so a name alone is not enough. + The key is kept in an ordered and in a hashed container, so it must + support `==`, `<` and `std::hash`. This function is optional; if not provided, `type` is assumed to be unique, and used as is. diff --git a/doc/modules/ROOT/pages/ref_headers.adoc b/doc/modules/ROOT/pages/ref_headers.adoc index d07713f5..a3141682 100644 --- a/doc/modules/ROOT/pages/ref_headers.adoc +++ b/doc/modules/ROOT/pages/ref_headers.adoc @@ -164,8 +164,13 @@ is defined. That policy needs no header - see above. ### link:{headers-url}/boost/openmethod/policies/static_rtti.hpp[] -Provides a minimal implementation of the `rtti` policy that does not depend on -standard RTTI. +Provides cpp:static_rtti[], a minimal implementation of the `rtti` policy that +does not depend on standard RTTI. + +### link:{headers-url}/boost/openmethod/policies/named_static_rtti.hpp[] + +Provides cpp:named_static_rtti[], a variant of cpp:static_rtti[] that works +across shared libraries. ### link:{headers-url}/boost/openmethod/policies/throw_error_handler.hpp[] diff --git a/doc/modules/ROOT/pages/shared_libraries.adoc b/doc/modules/ROOT/pages/shared_libraries.adoc index bd9b26ab..6522221e 100644 --- a/doc/modules/ROOT/pages/shared_libraries.adoc +++ b/doc/modules/ROOT/pages/shared_libraries.adoc @@ -2,178 +2,129 @@ [#shared_libraries] -OpenMethod interoperates with shared libraries on Linux, other POSIX-like -platforms, and Windows. +OpenMethod works with shared libraries on Linux, other POSIX-like platforms, +and Windows. A program and its shared libraries can all contribute classes, +methods and overriders to the same registry, provided they share its state. -OpenMethod uses global data to keep track of methods, overriders and classes, -all managed by static constructors and destructors. cpp:initialize[] uses that -information to set up dispatch tables. For a program and its shared libraries -to contribute classes, methods and overriders to the same registry, that data -must be truly global: a single copy, shared by all the modules. +## Sharing a Registry -All of a registry's mutable state lives in a single variable. Sharing a -registry across modules therefore means sharing that one symbol: the _owning_ -module exports it, and every _client_ module imports it. Three macros do this. -Each takes the registry as an argument, so they can be used to manage -`default_registry`, `indirect_registry` and any custom registry: +All of a registry's mutable state lives in a single variable. One module +_owns_ it and exports it; every other module - a _client_ - imports it. Three +macros do this. Each takes the registry as an argument, so they work the same +for `default_registry`, `indirect_registry` and custom registries: [cols="2,3"] |=== | Macro | Where | xref:reference:BOOST_OPENMETHOD_IMPORT_REGISTRY.adoc[BOOST_OPENMETHOD_IMPORT_REGISTRY] -| header; every translation unit of a _client_ module +| every translation unit of a client module | xref:reference:BOOST_OPENMETHOD_EXPORT_REGISTRY.adoc[BOOST_OPENMETHOD_EXPORT_REGISTRY] -| header; every translation unit of the _owning_ module +| every translation unit of the owning module | xref:reference:BOOST_OPENMETHOD_INSTANTIATE_REGISTRY.adoc[BOOST_OPENMETHOD_INSTANTIATE_REGISTRY] -| exactly one `.cpp` of the owning module +| exactly one translation unit of the owning module |=== -The owning module uses two of them: `EXPORT` in the header its translation -units share, and `INSTANTIATE` in exactly one of them. +The examples below put `IMPORT` and `EXPORT` in a header that every +translation unit includes, and choose between them with a macro that the +owning module defines: [source,c++] ---- -// header, every translation unit of a client module -BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry); -// header, every translation unit of the owning module +#ifdef OWNS_REGISTRY_STATE BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry); -// exactly one .cpp of the owning module -BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); +#else +BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry); +#endif ---- -Use them at namespace scope, after `` has been included -and after the registry class is defined - they expand to -`registry_state`, which needs a complete type. -Everything they emit is fully qualified, so nothing need be added to +The owner then adds `INSTANTIATE` to one of its `.cpp` files. Use the macros +at namespace scope, after `` and the definition of the +registry. They emit fully qualified names, so nothing needs to be added to `namespace boost::openmethod`. -The macros exist because the underlying explicit instantiations are _not_ -portable - the two ABIs want opposite things, and getting it wrong fails on one -platform while compiling silently on the other: - -* On Windows, Cygwin and MinGW, `__declspec(dllexport)` and `extern` are - incompatible on an explicit instantiation, and MSVC rejects the combination - outright. Nor is such a declaration needed there: visibility is not a PE - concept, so the owning module's other translation units may instantiate the - state implicitly. `EXPORT` expands to nothing and `INSTANTIATE` carries the - `dllexport`. -* On ELF and Mach-O the attribute must be on the _declaration_, so that every - translation unit of the owning module pins the symbol to default visibility. - Repeating it on the definition is an error on GCC, so `INSTANTIATE` carries - no attribute there. - -WARNING: on ELF, `EXPORT` is not decoration. A translation unit of the owning -module that has neither it nor the instantiation instantiates the state -implicitly, and under `-fvisibility=hidden` that copy is module-local. Because -ELF merges COMDATs at the _most restrictive_ visibility, the merged symbol then -becomes local: the module builds, exports nothing, and clients fail to link with -an undefined reference to `registry_state<...>::st`. +WARNING: a translation unit that uses neither macro still compiles, with a +private copy of the state. In a client module, that module then stops sharing +classes, methods and overriders with the rest of the program, which shows as +`missing_class` or unexpected `bad_call` errors. In the owning module, under +`-fvisibility=hidden` on ELF, it hides the state: the module exports nothing, +and clients fail to link with an undefined reference to +`registry_state<...>::st`. Putting the macros in a header, as above, prevents +both. -Nothing else needs decoration. In particular, methods declared with -`BOOST_OPENMETHOD` require no export or import: method objects are consolidated -across modules when `initialize` runs (see <>). +Do not replace the macros with hand-written explicit instantiations. Different +platforms need different decorations, and the wrong one compiles silently on +some of them (see <>). -If a library uses open-methods as an implementation detail, and does not make -them part of its public API, it is not necessary to export the registry state. +Nothing else needs decorating. Methods, overriders and class registrations are +consolidated across modules by cpp:initialize[] (see <>). +A library that uses open-methods only internally, and not in its API, does not +need to share the state at all. ## Implicit Linking -When a program links against a shared library at build time, all the modules -are loaded before `main` starts, their static constructors run, and a single -call to `initialize` sets up the dispatch tables. The only requirement is the -one stated above: one module owns the registry state, the others import it. - -The natural arrangement is for the shared library to own the registry: the -program already links against the library, so, on Windows, the import library -dependency runs in the usual direction. The `implicit_linking` example -demonstrates this arrangement. +When a program links against a shared library at build time, every module is +loaded, and its static constructors have run, before `main` starts. One call +to `initialize` then sets up the dispatch tables. -The following header is shared between the program and the library. It defines -the class hierarchy and the `meet` method, and it is also where each module says -how it relates to the registry state: +The library owns the state. The program links against the library, and on +Windows and macOS, which resolve imported symbols at link time, the library +cannot also import the state from the program: each would have to link against +the other. In the `implicit_linking` example, the header shared by both modules +defines the classes and the `meet` method, and selects `EXPORT` or `IMPORT`: [source,c++] ---- include::{shared}/implicit_linking/animals.hpp[tag=content] ---- -The module that owns the state defines `OWNS_REGISTRY_STATE` before including -this header, which selects `EXPORT`; every other module gets `IMPORT`. The owner -also uses `INSTANTIATE`, in one of its `.cpp` files. - -TIP: each module here has a single translation unit, so on ELF the owning module -could get away with `INSTANTIATE` alone. `EXPORT` is shown because it becomes -required the moment that module gains a second translation unit - see the -warning above. Putting it in the shared header from the start makes the module -safe to grow. - -Here the library owns the state. It registers the classes, provides the default -behaviour, and emits the definition: +The library defines `OWNS_REGISTRY_STATE` and instantiates the state. It +registers the classes, and provides the default overrider: [source,c++] ---- include::{shared}/implicit_linking/extensions.cpp[tag=content] ---- -The program extends it. It adds a class the library has never heard of, two -overriders that specialise `meet`, and imports the state through the header. It -also registers the library's classes a second time - see below: +The program imports the state, and adds a class and two overriders: [source,c++] ---- include::{shared}/implicit_linking/main.cpp[tag=content] ---- -Note what `next` does here: the program's `Herbivore, Carnivore` overrider calls -the library's `Animal, Animal` overrider, so control crosses the module boundary -in the middle of a single dispatch, and `cow meets wolf` prints -`do not greet, run`. - -The program registers `Animal`, `Herbivore`, `Cow`, `Carnivore` and `Wolf`, -although the library has registered them already. That is the rule -cpp:use_classes[] states: a class must be registered in as many translation -units as it has type ids. On ELF and Mach-O it has one - the dynamic linker -merges the `type_info` objects, and the second registration is redundant but -harmless. Windows keeps one `type_info` per module, so the program's `Wolf` and -the library's `Wolf` are two different type ids, and the program creates its -`Wolf` with `new`. Without the second registration the program's id is unknown -to the registry, and the first dispatch on it fails with `unknown class Wolf`. -Registering in both modules is what makes the example portable, and costs -nothing where it is not needed. - -## Dynamic Linking - -By "dynamic linking", we mean a program loading a shared library after it has -started, and accessing its content. A common application of dynamic linking is -to implement plugin architectures. +`next` crosses the module boundary in the middle of a dispatch: the program's +`Herbivore, Carnivore` overrider calls the library's `Animal, Animal` +overrider, so `cow meets wolf` prints `do not greet, run`. -A dynamically loaded library can add classes, methods and overriders to an -existing registry. `initialize` must be called to rebuild the dispatch tables -after loading or unloading a shared library. +The program registers the library's classes again, because a class must be +registered in every module that has its own type id for it (see +cpp:use_classes[]). Where the dynamic linker merges a class's `type_info` +objects - on ELF, for a class with default visibility - the second +registration is redundant but harmless. Elsewhere, and on Windows in +particular, each module has its own `type_info`; without the second +registration, the program's `Wolf`, created with `new`, is unknown to the +registry, and dispatching on it fails with `unknown class Wolf`. -Here the program is the natural owner of the registry state: it must be able -to run before the library is loaded, and regardless of whether it is loaded at -all. +## Dynamic Linking -Let's look at an example. It shares the same `animals.hpp` as the implicit-linking -example above - same classes, same `meet` method, same `OWNS_REGISTRY_STATE` -convention - but ownership is reversed. +A program can also load a shared library after it has started - a plugin, +typically - and the library can add classes, methods and overriders to the +registry. Call `initialize` again after loading or unloading a library. -Here the main program owns the registry state, so it is `main.cpp` that defines -`OWNS_REGISTRY_STATE` and emits the `EXPORT` definition, in its only translation -unit, satisfying the exactly-one rule. It provides a catch-all overrider, then -calls the `meet` method: +The program is then the natural owner of the state: it must work before the +library is loaded, and whether or not it is. The `dynamic_loading` example uses +a header like the one above. The program defines `OWNS_REGISTRY_STATE`, +instantiates the state, provides a catch-all overrider, and calls `meet`: [source,c++] ---- include::{shared}/dynamic_loading/main.cpp[tag=before] ---- -The shared library does not define `OWNS_REGISTRY_STATE`, so the header -imported the state for it. It adds two overriders, a new class, `Tiger`, and a +The library imports the state. It adds two overriders, a class, `Tiger`, and a factory function: [source,c++] @@ -181,38 +132,35 @@ factory function: include::{shared}/dynamic_loading/extensions.cpp[tag=content] ---- -We load the shared library using Boost.DLL. After calling `initialize`, the new -overriders are installed. The main program can also use `Tiger` objects, even -though it has no knowledge of that class at compile time. +Once the library is loaded with Boost.DLL and `initialize` has run, its +overriders are in effect, and the program can use `Tiger` objects without +knowing the class: [source,c++] ---- include::{shared}/dynamic_loading/main.cpp[tag=load] ---- -Finally, we unload the shared library and call `initialize` again. The -overriders provided by the shared library are removed from the method. +Once it is unloaded and `initialize` has run again, they are gone: [source,c++] ---- include::{shared}/dynamic_loading/main.cpp[tag=unload] ---- -NOTE: `dlclose` does not necessarily unload the library when using gcc's -`-rdynamic` option. The library may remain loaded until the program exits, so -the output of the last `meet` call may still be the same as the previous one. +NOTE: with GCC's `-rdynamic`, `dlclose` does not necessarily unload the +library. It may stay loaded until the program exits, and the last `meet` call +then prints the same as the previous one. -NOTE: if a shared library installs an error handler (see -xref:error_handling.adoc[]) via the registry's `error_handler::set`, reset it -(e.g. to the previous handler) before the library is unloaded. The handler is -part of the registry's shared state, so it stays installed after `dlclose`; -calling it afterwards invokes code in the unloaded library. +NOTE: a library that installs an error handler (see +xref:error_handling.adoc[]) with the registry's `error_handler::set` must +restore the previous one before it is unloaded. The handler is part of the +shared state, and would otherwise point into unloaded code. ### CMake Setup -Since the executable owns the registry state, the shared library must link -against the executable to resolve the imported symbol. This is the reverse of -the typical linking direction: +The library imports a symbol that the executable defines, so it links against +the executable - the reverse of the usual direction: [source,cmake] ---- @@ -224,108 +172,90 @@ add_library(my_plugin SHARED extensions.cpp) target_link_libraries(my_plugin PRIVATE Boost::openmethod my_app) ---- -On Windows, `ENABLE_EXPORTS ON` on the executable tells CMake to generate an -import library (`.lib` on MSVC) that the DLL links against. On POSIX, -`ENABLE_EXPORTS` adds `-rdynamic` (or the platform's equivalent), which makes -the executable's symbols visible to the libraries it loads. - -NOTE: `BOOST_OPENMETHOD_INSTANTIATE_REGISTRY` produces an explicit instantiation -_definition_, which may appear only once in the program. Use it in exactly one -translation unit of the owning module; that module's other translation units use -`EXPORT`, and client modules `IMPORT`. - -WARNING: a translation unit that uses neither macro compiles cleanly - it just -silently gets its own private copy of the registry state instead of an error. -That module then stops sharing classes, methods and overriders with the rest of -the program (symptoms: `missing_class` or unexpected `bad_call` errors). Guard -against this by putting the applicable macro in a project header that every -translation unit includes, as in the examples, rather than repeating it in -individual `.cpp` files. +`ENABLE_EXPORTS ON` makes CMake generate an import library for the executable +on Windows, and pass `-rdynamic` (or its equivalent) on POSIX, so that the +executable's symbols are visible to the libraries it loads. [#type_ids_across_modules] ## Type Ids Across Modules -Everything above is about sharing the registry's _state_. There is a second, -quieter question: where the _type ids_ themselves come from, and how far apart -they end up. It decides how well the registry's cpp:type_hash[] policy can do -its job, and it is the one place where `dlopen` behaves differently from -ordinary linking. - -Under cpp:std_rtti[], a type id is `&typeid(X)` - the address of a -`std::type_info` object. The Itanium ABI requires that identity to be _pointer_ -identity across modules, and the linker delivers it for an implicitly linked -shared library with a copy relocation: the record is copied into the -executable's image, and the library's references are redirected to that copy. -So a program and the libraries it links against present one compact set of type -ids, however many modules there are. - -`dlopen` does not get that. A plugin's own classes are not named by the -executable, so nothing unifies them; their records stay in the plugin's own -mapping, which the loader places wherever it likes - and with address-space -randomization, somewhere different on every run. The distance between a -program's type ids and its plugin's is routinely measured in terabytes, and it -moves from run to run. - -NOTE: RTTI has to keep default visibility for ids to unify at all. Under -`-fvisibility=hidden` one class can end up with a different `type_info` object -in each module; cpp:initialize[] copes - it treats them as several ids for the -same class - but they are extra ids for the hash to separate. This is why the -library's own shared-library tests mark their classes `BOOST_SYMBOL_VISIBLE`. +Sharing the state is one question. Where the type ids come from is another: it +decides how well the registry's cpp:type_hash[] policy does its job, and it is +where `dlopen` differs from implicit linking. + +Under cpp:std_rtti[], a type id is `&typeid(X)`. On ELF, for an implicitly +linked library, the dynamic linker gives a class a single `type_info` across +modules, by copying the record into the executable and redirecting the +library's references to the copy. A program and its libraries thus present one +compact set of type ids. The classes of a `dlopen`{empty}ed plugin get no such +treatment: their records stay in the plugin's mapping, which the loader places +anywhere - with address-space randomization, somewhere different on every run, +and often terabytes away from the program's. + +NOTE: under `-fvisibility=hidden`, a class can have a different `type_info` in +each module. cpp:initialize[] treats them as several ids for the same class, +but each one is one more id for the hash to separate. On ELF, declaring the +class `BOOST_SYMBOL_VISIBLE` gives it a single `type_info` again. + +NOTE: under cpp:static_rtti[], a type id is the address of a static variable in +a template of the library. Under `-fvisibility=hidden`, and always with +Windows DLLs, each module has its own: cpp:initialize[] takes the copies of a +class for unrelated classes, and a call made in one module on an object created +in another lands in the wrong overrider. Use cpp:named_static_rtti[], which +maps the ids to the names of the classes so that cpp:initialize[] merges them. ### What it costs -cpp:fast_perfect_hash[], the default, searches for a multiplier `M` and a shift -`S` such that `(M * x) >> S` is collision-free over the registered type ids. It -is fast and compact when the ids are evenly spread, and degrades when they are -not - and a program plus a few `dlopen`{empty}ed modules is as uneven as it -gets: several tight clusters, very far apart. Two things follow: - -* the search gets dramatically more expensive, and on a large enough set it - fails - it gives up after half a million attempts and the error handler is - called with a `search_error`, which by default terminates the program; -* cpp:vptr_vector[] sizes its table from the hash's range, so a hash that is - working hard costs memory as well as time. +cpp:fast_perfect_hash[], the default, looks for a multiplier `M` and a shift +`S` such that `(M * x) >> S` is collision-free over the type ids. It does well +when the ids are evenly spread, and badly on a program with a few plugins: +several tight clusters, far apart. The search becomes much more expensive, and +on a large enough set it gives up after half a million attempts, calling the +error handler with a `search_error` - which terminates the program by default. +And since cpp:vptr_vector[] sizes its table from the hash's range, a struggling +hash costs memory as well as time. -A program that loads plugins and registers more than a few hundred classes is -the one most likely to meet both. +A plugin host that registers more than a few hundred classes is the program +most likely to meet both problems. A plugin that registers no classes of its +own, and only adds overriders for the program's, contributes no type ids, and +does not cause them. ### The alternatives -Three other cpp:type_hash[] policies trade that away, and a fourth option -removes the hash from the picture entirely. They are all drop-in: `with` -replaces a policy with the one of the same category, in place, so the new hash -still precedes cpp:vptr_vector[] in the list. +Three other cpp:type_hash[] policies avoid the problem, and cpp:vptr_map[] +removes the hash altogether: [cols="1,3"] |=== | policy | what it does a| cpp:minimal_perfect_hash[] -a| One slot per type id, whatever the addresses are, and a search whose cost -depends only on how many classes there are. The table size can be stated before -seeing an address. Costs a second dependent load on every dispatch - a -nanosecond or two per call. **The one to reach for in a plugin host.** +a| One slot per type id, whatever the addresses, and a search whose cost +depends only on the number of classes. Costs a second dependent load on every +dispatch - a nanosecond or two per call. **The one to reach for in a plugin +host.** a| cpp:two_level_hash[] -a| The same idea with the final reduction replaced by a shift. Cheaper per call -than `minimal_perfect_hash` where the compiler hoists the shift amount out of -the dispatch loop, at the price of a table that rounds up to a power of two - -between one and two slots per type id, depending on the class count. +a| The same idea, with the final reduction replaced by a shift. Cheaper per call +than `minimal_perfect_hash` where the compiler hoists the shift out of the +dispatch loop, but the table rounds up to a power of two: between one and two +slots per type id. a| cpp:minimal_cover_hash[] a| Indexes by the smallest set of bit positions that still separates the type -ids. As fast per call as the default, and it finds its table deterministically -in milliseconds. Needs BMI2, for **every** translation unit of the program - -see its documentation before choosing it. +ids. As fast per call as the default, and finds its table deterministically, in +milliseconds. Needs BMI2 in **every** translation unit of the program - see its +documentation before choosing it. a| cpp:vptr_map[] -a| Not a hash at all: a map keyed on the type id, so there is no table to size -and no search to fail. Slower per dispatch than any of the above, and the only -option that asks nothing of the type ids. +a| Not a hash: a map keyed on the type id, so there is no table to size and no +search to fail. Slower per dispatch than all of the above, and the only option +that asks nothing of the type ids. |=== -Switching is one declaration. The registry is then a custom registry, so it -needs the treatment in <> to be shared across modules: +Switching takes one declaration, since `with` replaces a policy with the one of +the same category, in place. The registry is then a custom registry, shared as +described in <>: [source,c++] ---- @@ -334,8 +264,8 @@ struct plugin_registry : boost::openmethod::policies::minimal_perfect_hash<>> {}; ---- -`vptr_map` replaces the `vptr` policy rather than the hash, and the hash is then -dead weight, so drop it: +`vptr_map` replaces the `vptr` policy rather than the hash, which is then dead +weight, so drop it: [source,c++] ---- @@ -345,59 +275,46 @@ struct plugin_registry : boost::openmethod::policies::type_hash> {}; ---- -TIP: none of this arises until a module registers classes of its own. A plugin -that only adds _overriders_ for classes the program already registered -contributes no new type ids, and the default policies are as good there as -anywhere. - ## Indirect Vptrs -`initialize` rebuilds the v-tables in the registry. This invalidates all the -`virtual_ptr`{empty}s, and also the v-table pointers stored in objects by -cpp:inplace_vptr_base[], related to that registry. This is seldom an issue, as -most programs that dynamically load shared libraries do so at the very beginning -of their execution. - -Otherwise, indirect v-table pointers must be used. This is achieved by using a -registry that contains the cpp:indirect_vptr[] policy. The library provides an -cpp:indirect_registry[] that has the same policies as `default_registry`, plus -`indirect_vptr`. Make it the registry the `BOOST_OPENMETHOD` macros use by -defining +`initialize` rebuilds the registry's v-tables, which invalidates its +`virtual_ptr`{empty}s, and the v-table pointers that cpp:inplace_vptr_base[] +stores in objects. Most programs load their shared libraries at the very +beginning, before any of those exist, and need not care. + +The others need a registry with the cpp:indirect_vptr[] policy, such as +cpp:indirect_registry[]: `default_registry` plus `indirect_vptr`. To make it +the registry that the `BOOST_OPENMETHOD` macros use, define xref:reference:BOOST_OPENMETHOD_DEFAULT_REGISTRY.adoc[BOOST_OPENMETHOD_DEFAULT_REGISTRY] -_before_ including ``, or any header that includes -it, like ``. +_before_ including ``, or a header that includes it, +like ``. -The `indirect_vptr` example does that in the header both modules share, rather -than passing a compiler switch from the build system. One place then settles -both questions - which registry the program uses, and how its state is shared - -and no translation unit can accidentally disagree: +The `indirect_vptr` example does it in the header that both modules share, so +that one place settles both questions - which registry, and how its state is +shared - and no translation unit can disagree. `indirect_registry` has its own +state, shared with the same three macros: [source,c++] ---- include::{shared}/indirect_vptr/animals.hpp[tag=content] ---- -cpp:indirect_registry[] has its own state, separate from that of -`default_registry`, shared with the same three macros: just name -`indirect_registry`, as above. - -Here is a program that carries `virtual_ptr`{empty}s across `initialize` calls. -It owns the state, so it defines `OWNS_REGISTRY_STATE` and emits the definition: +The program owns the state, and carries `virtual_ptr`{empty}s across +`initialize` calls: [source,c++] ---- include::{shared}/indirect_vptr/main.cpp[tag=content] ---- -The shared library it loads includes the same header, so it uses -`indirect_registry` too and imports the state. The complete example is in the -`indirect_vptr` directory. +The library it loads includes the same header, and imports the state. The +complete example is in the `indirect_vptr` directory. [#custom_registries] ## Custom Registries -A custom registry is shared exactly the same way - name it instead of -`default_registry`. Nothing above is specific to the predefined registries: +A custom registry is shared the same way - name it instead of +`default_registry`: [source,c++] ---- @@ -421,11 +338,6 @@ BOOST_OPENMETHOD_IMPORT_REGISTRY(my_registry); #endif ---- -The `#define` makes `my_registry` the registry the `BOOST_OPENMETHOD` macros -use, exactly as `indirect_vptr/animals.hpp` does above; drop it if the methods -name their registry explicitly. The sharing macros come last, after the -definition. - [source,c++] ---- // registry.cpp - exactly one translation unit of the owning module @@ -435,91 +347,91 @@ definition. BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(my_registry); ---- +The `#define` makes `my_registry` the registry that the `BOOST_OPENMETHOD` +macros use; drop it if the methods name their registry explicitly. The sharing +macros come after the definition. + [#implementation_notes] ## Implementation Notes ### One state variable per registry -Everything mutable in a registry is agglomerated into a single variable. The -registry's class and method lists, the dispatch tables, and the state of every -stateful policy (for example, the hash function parameters of -cpp:fast_perfect_hash[], or the vector of v-table pointers of -cpp:vptr_vector[]) are all members of one object, of which there is exactly -one instance per registry. Policies do not keep their own globals: their state -is gathered, by template machinery, into a tuple inside that same object. +Everything mutable in a registry is a member of one object: the class and +method lists, the dispatch tables, and the state of every stateful policy - for +example, the hash parameters of cpp:fast_perfect_hash[], or the v-table +pointers of cpp:vptr_vector[]. Policies keep no globals of their own; template +machinery gathers their state into a tuple inside that object. Whatever the +policies, there is exactly _one_ symbol to export and import. -This is what makes sharing a registry across modules tractable: there is -exactly _one_ symbol to export and import, whatever the registry's policies. The variable is the sole member of cpp:registry_state[], a deliberately thin, -function-free class template. That form is dictated by MSVC: +function-free class template, a form dictated by MSVC. `dllexport`/`dllimport` are only honored on a _whole-class_ explicit -instantiation - not on a variable template, nor on the instantiation of a -lone static data member - and exporting a class wholesale also decorates its -member functions and nested types, which the state-holding class itself could -not tolerate. A one-member wrapper class with no functions is the one form -that can be exported and imported on every platform. +instantiation - not on a variable template, nor on a lone static data member - +and exporting a class decorates its member functions and nested types too, +which the class holding the state could not tolerate. A one-member class +without functions is the one form that every platform can export and import. -### Registrars are consolidated, not shared +### Registrations are consolidated, not shared -The export/import mechanism is deliberately limited to that single state -variable. No attempt is made to share the _registrars_ - the static objects +Only the state variable is shared. The _registrars_ - the static objects generated by `BOOST_OPENMETHOD`, `BOOST_OPENMETHOD_OVERRIDE` and -`BOOST_OPENMETHOD_CLASSES` - across modules. Each module keeps its own copies -of the method objects, of the registration records, and of the per-class -v-table pointers used for dispatch. +`BOOST_OPENMETHOD_CLASSES` - are not: each module keeps its own method objects, +registration records, and per-class v-table pointers. -Instead, the copies are consolidated at runtime. When a module is loaded, its +The copies are consolidated at run time instead. When a module is loaded, its static constructors append its class and method records to the lists in the -shared registry state, each record pointing back into the module that -registered it. When `initialize` runs, records that describe the same method -or class - typically, one per module, keyed by type - are grouped together. -The dispatch tables are computed once, from the consolidated information; then -the resulting slots and strides are copied back into _every_ module's copy of -each method object, and every module's v-table pointers are updated through -the pointers captured at registration time. Each module thus dispatches -through its own statics, at full speed, but all the copies agree because they -were derived from the same computation. - -This is why method declarations need no `dllexport`/`dllimport` arguments, and -why modules do not need to agree on who "owns" a method: ownership only exists -for the registry state. +shared state, each record pointing back into its module. `initialize` groups +the records that describe the same class or method, computes the dispatch +tables once, then copies the resulting slots and strides into every module's +copy of each method, and updates every module's v-table pointers through the +pointers captured at registration. Each module dispatches through its own +statics, at full speed, and all the copies agree. +This is why methods need no `dllexport` or `dllimport`, and why modules need +not agree on who "owns" a method: only the registry state has an owner. + +[#platform_details] ### Platform Details -On Windows, by default, each module (executable or DLL) receives its own copy -of global variables. Without special measures, the registry state, dispatch -tables, and method function pointers are duplicated: when a DLL's static -constructors register classes and overriders, they populate the DLL's own copy -of the registry, invisible to the main program. The `EXPORT` and `IMPORT` -macros expand to `+__declspec(dllexport)+` and `+__declspec(dllimport)+` -decorations on an explicit instantiation of the registry state, making the -owner's copy the only one. - -On Linux and other ELF platforms, the macros can sometimes be omitted. Without -them, the registry state is an implicitly instantiated template variable with -ordinary external linkage, and the dynamic linker collapses all the modules' -copies into one - provided the symbol is visible, which is what `-rdynamic` -(`ENABLE_EXPORTS ON` in CMake) ensures for an executable. However, if the -program is built with `-fvisibility=hidden`, each module's copy is internalized -to a module-local symbol, and the modules end up with separate registries, just -like on Windows. The macros handle this case: `EXPORT` emits an explicit -instantiation with default visibility; `IMPORT` emits a plain `extern template` -declaration, which suppresses the client's own instantiation, forcing it to -reference the owner's symbol. Since the macros are harmless when they are not -needed, portable code should simply always use them. - -The platforms also differ in _when_ the imported symbol must be resolved. On -ELF, a shared library may be linked with unresolved references, which the -dynamic linker satisfies at load time; a plugin can thus import the registry -state from the executable that will load it. Mach-O (macOS) and PE (Windows) -require every reference to be satisfied at link time: a client library must -link against the owning module - hence the reverse link in the CMake setup -above. - -WARNING: on Windows, every module that shares a registry must be linked -against the same dynamic C++ runtime (the `/MD` or `/MDd` family). The -registry state contains standard containers (`std::vector`, `std::function`, -etc.) allocated by whichever module runs `initialize`; if a module links the -static runtime (`/MT`) instead, or links a different runtime version, that -module's CRT frees memory it did not allocate when the state is destroyed at -exit, corrupting the heap. +On Windows, each module - executable or DLL - has its own copy of a global +variable by default. Without the macros, a DLL's static constructors would +register its classes and overriders in the DLL's own copy of the state, +invisible to the program. + +On ELF, the dynamic linker merges the modules' copies of an implicitly +instantiated variable into one - provided it is visible, which `-rdynamic` +(`ENABLE_EXPORTS ON` in CMake) ensures for an executable. Under +`-fvisibility=hidden`, each copy stays local to its module, and the modules end +up with separate registries, as on Windows. + +The two need different decorations, which is why the macros branch on the +platform: + +* On Windows, Cygwin and MinGW, `__declspec(dllexport)` and `extern` cannot be + combined on an explicit instantiation, and the owner's other translation + units may instantiate the state implicitly, since visibility is not a PE + concept. So `EXPORT` expands to nothing, and `INSTANTIATE` carries the + `dllexport`. +* On ELF and Mach-O, the attribute must be on the _declaration_, so that every + translation unit of the owner gives the symbol default visibility. GCC + rejects it if repeated on the definition, so `INSTANTIATE` carries none. + An implicit instantiation in a translation unit without `EXPORT` would hide + the symbol: ELF merges COMDATs at the _most restrictive_ visibility. + +`IMPORT` is an `extern template` declaration everywhere - with `dllimport` on +Windows - which keeps a client from instantiating the state and makes it use +the owner's. + +The platforms also differ in _when_ the imported symbol is resolved. On ELF, a +shared library may be linked with unresolved references, which the dynamic +linker satisfies at load time, so a plugin can import the state from the +executable that loads it. Mach-O and PE resolve every reference at link time, +so a client library must link against the owning module - hence the reverse +link in the CMake setup above. + +WARNING: on Windows, every module that shares a registry must use the same +dynamic C++ runtime (the `/MD` or `/MDd` family). The state contains standard +containers, allocated by whichever module runs `initialize`. A module that +links the static runtime (`/MT`), or another version of the runtime, frees +memory it did not allocate when the state is destroyed at exit, which corrupts +the heap. diff --git a/include/boost/openmethod/core.hpp b/include/boost/openmethod/core.hpp index fa551ff3..6b46057c 100644 --- a/include/boost/openmethod/core.hpp +++ b/include/boost/openmethod/core.hpp @@ -1134,7 +1134,8 @@ inline auto final_virtual_ptr(Arg&& obj) { // check that dynamic type == static type auto static_type = Registry::rtti::template static_type(); - auto dynamic_type = Registry::rtti::dynamic_type(Traits::peek(obj)); + auto dynamic_type = + detail::dynamic_type_of(Traits::peek(obj)); // Equal type_ids settle it, and that is the common case: both come // from this module. Only when they differ is type_index needed, because @@ -2263,37 +2264,108 @@ using overrider_virtual_types = boost::mp11::mp_remove< MethodParameters, OverriderParameters>, void>; -template -struct init_bad_call { - template - static auto fn(bad_call& error, const Arg& arg, const Args&... args) { - if constexpr (Index == 0u) { - error.method = Rtti::template static_type(); - error.arity = sizeof...(args) + 1; +// The class of an object, found from its v-table pointer, for an rtti policy +// that cannot tell the dynamic type. Used only to report an error. +// +// The object's own class always matches, because the v-table pointer came from +// its `static_vptr`, so a single match is the answer. Another class may match +// as well: a v-table whose leading slots are unused is pointed to before its +// start, possibly at the start of another class' v-table. Then `declared` is +// the answer: it is less precise, but true. +template +auto class_of_vptr(vptr_type vptr, type_id declared) -> type_id { + using rtti = typename Registry::rtti; + + if (!vptr) { + return declared; + } + + // `found` is only meaningful once `matched` is set: a `type_id` is opaque, + // and a policy may well use a null one. + type_id found = declared; + bool matched = false; + + for (auto& ci : Registry::state().classes) { + if (*ci.static_vptr != vptr) { + continue; + } + + if (!matched) { + found = ci.type; + matched = true; + } else if (rtti::type_index(ci.type) != rtti::type_index(found)) { + return declared; } + } + + return found; +} + +// The class of a virtual argument, for a `bad_call`, read through the routes +// `method::vptr` takes to dispatch it. `Parameter` is the parameter's type, +// stripped of `virtual_`: its `virtual_traits` may read the class of a value a +// type-erased argument (an `any`) holds. +template +auto bad_call_type(const Arg& arg) -> type_id { + using rtti = typename Registry::rtti; + using traits = virtual_traits; - type_id arg_type_id; + if constexpr (is_virtual_ptr) { + using Class = std::remove_cv_t; - if constexpr (is_virtual_ptr) { - arg_type_id = Rtti::dynamic_type(*arg); + if constexpr (rtti::template is_polymorphic) { + return dynamic_type_of(*arg); } else { - arg_type_id = Rtti::dynamic_type(arg); + return class_of_vptr( + arg.vptr(), rtti::template static_type()); } + } else if constexpr (rtti::template is_polymorphic) { + return dynamic_type_of(arg); + } else if constexpr (has_adl_vptr) { + return class_of_vptr( + boost_openmethod_vptr(arg, static_cast(nullptr)), + rtti::template static_type()); + } else if constexpr (has_vptr) { + return class_of_vptr( + traits::vptr(arg), rtti::template static_type()); + } else { + return rtti::template static_type(); + } +} - error.types[Index] = arg_type_id; +// Records the class of each virtual argument, in order, as dispatch sees +// them: the other arguments play no part in it, so `types[i]` is the class of +// the i-th *virtual* argument. `ParameterList` is the method's parameters, +// walked in step with the arguments, as `resolve_uni` does. +template +void init_bad_call_types(bad_call&) { +} - init_bad_call::fn(error, args...); - } +template< + class Registry, std::size_t Index, class ParameterList, typename Arg, + typename... Args> +void init_bad_call_types(bad_call& error, const Arg& arg, const Args&... args) { + using parameter = boost::mp11::mp_first; + using rest = boost::mp11::mp_rest; + + if constexpr (is_virtual::value) { + if constexpr (Index < bad_call::max_types) { + error.types[Index] = + bad_call_type>(arg); + } - static auto fn(bad_call&) { + init_bad_call_types(error, args...); + } else { + init_bad_call_types(error, args...); } -}; +} -template -struct init_bad_call { - static auto fn(bad_call&) { - } -}; +template +void init_bad_call(bad_call& error, const Args&... args) { + error.method = Registry::rtti::template static_type(); + error.arity = boost::mp11::mp_count_if::value; + init_bad_call_types(error, args...); +} template using method_base = std::conditional_t< @@ -3072,7 +3144,7 @@ method::fn_not_implemented( if constexpr (Registry::has_error_handler) { no_overrider error; - detail::init_bad_call::fn( + detail::init_bad_call>( error, detail::parameter_traits::peek(args)...); Registry::error_handler::error(error); @@ -3090,7 +3162,7 @@ method::fn_ambiguous( if constexpr (Registry::has_error_handler) { ambiguous_call error; - detail::init_bad_call::fn( + detail::init_bad_call>( error, detail::parameter_traits::peek(args)...); Registry::error_handler::error(error); diff --git a/include/boost/openmethod/detail/type_key.hpp b/include/boost/openmethod/detail/type_key.hpp new file mode 100644 index 00000000..4627accd --- /dev/null +++ b/include/boost/openmethod/detail/type_key.hpp @@ -0,0 +1,51 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_DETAIL_TYPE_KEY_HPP +#define BOOST_OPENMETHOD_DETAIL_TYPE_KEY_HPP + +#include +#include +#include + +namespace boost::openmethod::detail { + +// A key that identifies a class across modules, for an rtti policy that +// identifies classes by name: the name, and for a class that cannot be shared +// across translation units - one with internal linkage, or none - the type_id +// as well, which keeps it apart from other classes with the same name. +struct type_key { + std::string_view name; + const void* local; + + friend auto operator==(const type_key& a, const type_key& b) -> bool { + return a.local == b.local && a.name == b.name; + } + + friend auto operator!=(const type_key& a, const type_key& b) -> bool { + return !(a == b); + } + + friend auto operator<(const type_key& a, const type_key& b) -> bool { + if (a.name != b.name) { + return a.name < b.name; + } + + return std::less()(a.local, b.local); + } +}; + +} // namespace boost::openmethod::detail + +template<> +struct std::hash { + auto operator()(const boost::openmethod::detail::type_key& key) const + -> std::size_t { + return std::hash()(key.name) ^ + std::hash()(key.local); + } +}; + +#endif diff --git a/include/boost/openmethod/policies/named_static_rtti.hpp b/include/boost/openmethod/policies/named_static_rtti.hpp new file mode 100644 index 00000000..3cc65b20 --- /dev/null +++ b/include/boost/openmethod/policies/named_static_rtti.hpp @@ -0,0 +1,293 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_POLICY_NAMED_STATIC_RTTI_HPP +#define BOOST_OPENMETHOD_POLICY_NAMED_STATIC_RTTI_HPP + +#include +#include + +#include + +#include +#include +#include +#include + +namespace boost::openmethod::detail { + +// The signature of this function, as the compiler prints it, contains the name +// of `T`. Its prefix and suffix are the same for every `T`; they are measured +// on `void`. +template +constexpr auto static_rtti_signature() -> const char* { + return BOOST_CURRENT_FUNCTION; +} + +inline constexpr std::string_view static_rtti_probe = + static_rtti_signature(); +inline constexpr std::size_t static_rtti_prefix = + static_rtti_probe.find("void"); + +template +constexpr auto static_rtti_printed_name() -> std::string_view { + // Without __PRETTY_FUNCTION__ or __FUNCSIG__, BOOST_CURRENT_FUNCTION falls + // back on a name that does not contain `T`, and every class would get the + // same name. + static_assert( + static_rtti_prefix != std::string_view::npos, + "named_static_rtti requires __PRETTY_FUNCTION__ or __FUNCSIG__"); + + constexpr std::string_view signature = static_rtti_signature(); + + // The name of `T` takes the place of `void` in the probe. + return signature.substr( + static_rtti_prefix, + signature.size() - static_rtti_probe.size() + + std::string_view("void").size()); +} + +// MSVC prints the class-key of every class it names, at the head of the name +// and inside template arguments: `struct Box`. Two +// modules that disagree on the key of a class - `class Dog;` in one and +// `struct Dog {...}` in the other, which is only a warning - would print two +// names for it, and the registry would not merge them. Dropping the keys also +// makes the name of a class the same on every compiler, which is what a +// diagnostic shows. +constexpr std::string_view static_rtti_class_keys[] = { + "struct ", "class ", "union ", "enum " +}; + +// A keyword is only one where a name cannot continue, or `structural` would +// lose its head. +constexpr auto static_rtti_name_char(char c) -> bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_'; +} + +// The length of the class-key starting at `pos`, or zero. +constexpr auto static_rtti_class_key_at(std::string_view name, std::size_t pos) + -> std::size_t { + if (pos > 0 && static_rtti_name_char(name[pos - 1])) { + return 0; + } + + for (auto key : static_rtti_class_keys) { + if (name.substr(pos, key.size()) == key) { + return key.size(); + } + } + + return 0; +} + +constexpr auto static_rtti_stripped_size(std::string_view name) -> std::size_t { + std::size_t size = 0; + + for (std::size_t pos = 0; pos < name.size();) { + if (auto key = static_rtti_class_key_at(name, pos)) { + pos += key; + } else { + ++pos; + ++size; + } + } + + return size; +} + +// Whether this compiler prints class-keys at all, asked of a class whose own +// name holds no answer. Only MSVC does, and only there is the stripping below +// worth an array per class. +struct static_rtti_probe_class {}; + +inline constexpr bool static_rtti_prints_class_keys = + static_rtti_class_key_at( + static_rtti_printed_name(), 0) != 0; + +// The printed name of `T` with the class-keys removed, in storage of its own, +// since it is no longer a substring of the signature. `type_index` compares +// the contents, not the address, so a copy per module costs nothing but space. +template +constexpr auto static_rtti_strip(std::string_view name) + -> std::array { + std::array result{}; + + for (std::size_t pos = 0, i = 0; pos < name.size();) { + if (auto key = static_rtti_class_key_at(name, pos)) { + pos += key; + } else { + result[i++] = name[pos++]; + } + } + + return result; +} + +template +struct static_rtti_stripped_name { + static constexpr auto size = + static_rtti_stripped_size(static_rtti_printed_name()); + + static constexpr std::array chars = + static_rtti_strip(static_rtti_printed_name()); +}; + +template +constexpr auto static_rtti_type_name() -> std::string_view { + // Where the compiler prints no class-key, the name is already the answer, + // and it costs nothing: a view into the signature, which is a string + // literal the program carries anyway. + if constexpr (static_rtti_prints_class_keys) { + using stripped = static_rtti_stripped_name; + + return {stripped::chars.data(), stripped::size}; + } else { + return static_rtti_printed_name(); + } +} + +constexpr auto static_rtti_is_qualifier(char c) -> bool { + return (c >= 'a' && c <= 'z') || c == ' ' || c == '&'; +} + +// A class with internal linkage, or none, may print the same name as a +// different class. Only the compilers know which classes they are; this guesses +// it from the marks they leave in the name. GCC prints a local class as +// `f()::L`, or `S::f() const::L`: a scope closed by a parenthesis, maybe +// followed by qualifiers. +constexpr auto static_rtti_marked_local(std::string_view name) -> bool { + constexpr std::string_view marks[] = { + "{anonymous}", // GCC + "(anonymous namespace)", // Clang + "`", // MSVC: `anonymous-namespace', `anonymous namespace' + " 0 && static_rtti_is_qualifier(name[i - 1])) { + --i; + } + + if (i > 0 && name[i - 1] == ')') { + return true; + } + } + + return false; +} + +// Whether the copies of `T`'s type_id in different modules designate the same +// type, and can be merged by `name`, the name of `T`. Called at run time: the +// scan of the name, evaluated at compile time, would cost more than the rest of +// the registration of a class. +template +auto static_rtti_shareable(std::string_view name) -> bool { +#if BOOST_OPENMETHOD_HAS_REFLECTION + if constexpr ( + std::is_class_v || std::is_union_v || std::is_enum_v) { + return std::meta::has_external_linkage(^^T); + } +#endif + + // Without reflection; or a function or pointer type, which has no linkage + // of its own, but may involve a class that has internal linkage. + return !static_rtti_marked_local(name); +} + +} // namespace boost::openmethod::detail + +namespace boost::openmethod::policies { + +//! Implementation of the `rtti` policy that identifies classes by name. +//! +//! `named_static_rtti` is a @ref static_rtti that works across modules. It +//! categorizes all types as non-polymorphic, and its type ids are addresses +//! that differ from one module to the next, like those of @ref static_rtti. In +//! addition, `type_index` maps them to the name of the class, as the compiler +//! prints it, so the registry merges the copies of a class coming from +//! different modules. +//! +//! All the modules must be built with the same compiler, because the names +//! must be spelled the same way. Dispatch does not use names at all. They are +//! read before `main`, once per class in each module, as the class registers +//! itself, and compared once, by @ref initialize. +//! +//! A class that has internal linkage, or none, is not shared: it is identified +//! by its `type_id`, as with @ref static_rtti. This covers the classes defined +//! in an anonymous namespace, local classes, lambdas, unnamed classes, and the +//! templates instantiated with any of these. With C++26 reflection, the policy +//! asks the compiler which classes they are. Otherwise, it recognizes them from +//! the marks that the compiler leaves in their names. +//! +//! @warning Clang and MSVC leave no mark in the name of a local class. Clang +//! prints it unqualified, and MSVC prints it as if the function were a +//! namespace (`main::Local`). Without reflection, two local classes that print +//! the same name are taken for the same class, and so is a local class that +//! prints the same name as a class at namespace scope. So is a class template +//! instantiated with an entity that has internal linkage, such as the address +//! of a `static` variable, with all compilers. +//! +//! @see [Custom RTTI](xref:ROOT:custom_rtti.adoc) +struct named_static_rtti : rtti { + //! A RttiFn metafunction. + //! + //! @tparam Registry The registry containing this policy. + template + struct fn : static_rtti::fn { + //! Returns the @ref type_id of `Class`. + //! + //! @tparam Class A class. + template + static auto static_type() -> type_id { + using namespace detail; + + // The type_id points to the key: the name of the class, and, if + // the class cannot be shared, the address of the key itself, + // which sets it apart from another class with the same name. + constexpr auto name = static_rtti_type_name(); + static const type_key id{ + name, static_rtti_shareable(name) ? nullptr : &id + }; + + return &id; + } + + //! Returns a key that identifies a class across modules. + //! + //! @param type A `type_id`. + //! @return The name of the class, and `type` itself if the class + //! cannot be shared. + static auto type_index(type_id type) { + return *static_cast(type); + } + + //! Writes the name of a class to a stream. + //! + //! @param type A `type_id`. + //! @param stream A stream to write to. + template + static void type_name(type_id type, Stream& stream) { + stream << static_cast(type)->name; + } + }; +}; + +} // namespace boost::openmethod::policies + +#endif diff --git a/include/boost/openmethod/policies/static_rtti.hpp b/include/boost/openmethod/policies/static_rtti.hpp index 248bca6a..8bcdb103 100644 --- a/include/boost/openmethod/policies/static_rtti.hpp +++ b/include/boost/openmethod/policies/static_rtti.hpp @@ -19,6 +19,13 @@ namespace boost::openmethod::policies { //! disabled. `virtual_ptr` must be constructed using @ref final_virtual_ptr (or //! its equivalents for smart pointers). //! +//! Each module of a program has its own copy of those variables, unless the +//! dynamic linker merges them, which it does not do on Windows, nor for symbols +//! with hidden visibility. A class used by several modules then has one +//! `type_id` per module, and the registry takes the copies for unrelated +//! classes. Use @ref named_static_rtti in a program that shares a registry +//! across modules. +//! //! @par Example //! //! Selecting the policy. The registry is declared before @@ -50,7 +57,7 @@ struct static_rtti : rtti { //! Returns the @ref type_id of `Class`. //! //! @tparam Class A class. - template + template static auto static_type() -> type_id { static char id; diff --git a/include/boost/openmethod/policies/std_rtti.hpp b/include/boost/openmethod/policies/std_rtti.hpp index 8acd6d89..60d455e0 100644 --- a/include/boost/openmethod/policies/std_rtti.hpp +++ b/include/boost/openmethod/policies/std_rtti.hpp @@ -7,13 +7,68 @@ #define BOOST_OPENMETHOD_POLICY_STD_RTTI_HPP #include +#include -#ifndef BOOST_NO_RTTI #include + +#ifndef BOOST_NO_RTTI +#include #include #include #endif +// Whether std::type_index identifies classes the way initialize() needs: equal +// for the copies of one class in different modules, and different for two +// classes with internal linkage that have the same name. See +// std_rtti::type_index. +// +// _MSC_VER, not BOOST_MSVC: what matters is MSVC's standard library, which +// clang-cl uses too. +#if defined(_MSC_VER) || (defined(BOOST_GCC) && defined(__GLIBCXX__)) +#define BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX 1 +#else +#define BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX 0 +#endif + +namespace boost::openmethod::detail { + +// Whether a type name mangled per the Itanium C++ ABI designates a type that +// a different type, in another translation unit, may share the name of. Only +// the marks that cannot occur in an identifier are recognized, so the answer +// does not depend on parsing the name: +// +// _GLOBAL__N the name of an anonymous namespace (a reserved identifier) +// $_ Clang's name for an unnamed type or a lambda: `$_0` +// . GCC's name for an unnamed type: `._anon_0` +// +// A bare `$` is not one of them: GCC and Clang accept it in an identifier +// unless `-fno-dollars-in-identifiers` is given, so a class named `Foo$Impl` +// would be taken for an unnamed one and never merged across modules. +// +// A class local to a static function, or a template instantiated with the +// address of a static variable, is not recognized - as on MSVC, whose +// decorated names do not tell those apart either. +inline auto itanium_unnamed_mark(std::string_view name) -> bool { + for ( + auto pos = name.find("$_"); pos != std::string_view::npos; + pos = name.find("$_", pos + 2)) { + auto next = pos + 2; + + if (next < name.size() && name[next] >= '0' && name[next] <= '9') { + return true; + } + } + + return false; +} + +inline auto itanium_internal_linkage(std::string_view name) -> bool { + return name.find("_GLOBAL__N") != std::string_view::npos || + name.find('.') != std::string_view::npos || itanium_unnamed_mark(name); +} + +} // namespace boost::openmethod::detail + namespace boost::openmethod::policies { //! Implements the @ref rtti policy using standard RTTI. @@ -55,13 +110,16 @@ struct std_rtti : rtti { //! Returns the dynamic @ref type_id of an object. //! - //! Returns `&typeid(obj)`, cast to `type_id`. + //! Returns `&typeid(obj)`, cast to `type_id`. `Class` must be + //! polymorphic, or `typeid` would yield the static type. //! //! @tparam Class A registered class. //! @param obj A reference to an instance of `Class`. //! @return The type_id of `obj`'s class. template static auto dynamic_type(const Class& obj) -> type_id { + static_assert(is_polymorphic); + return &typeid(obj); } @@ -79,33 +137,49 @@ struct std_rtti : rtti { reinterpret_cast(type)->name()); } - //! Returns a key that uniquely identifies a class. + //! Returns a key that identifies a class across modules. //! //! C++ does *not* guarantee that there is a single instance of //! `std::type_info` per type: a class used by several modules of a //! program typically has one per module. `type_index` maps a `type_id` //! to a key that compares equal for all the type ids of one class, //! which is what @ref initialize uses to group the registrations coming - //! from different modules. + //! from different modules. The key also tells apart two classes with + //! the same name in different translation units, when they are defined + //! in anonymous namespaces, or are unnamed classes or lambdas. + //! + //! The type of the key is unspecified; it supports `==`, `<` and + //! `std::hash`. With MSVC, and with GCC and libstdc++, it is a + //! `std::type_index`: their `std::type_info` equality compares names, + //! except where the name of a class is shared by another one, in a + //! different translation unit. Elsewhere it is not: libc++ compares the + //! `std::type_info` objects of a class with hidden visibility by + //! address, so the copies in different modules are unequal, and Clang + //! does not mark the classes with internal linkage for libstdc++. The + //! key is then the mangled name, plus the `type_id` if the mangled name + //! shows an anonymous namespace, an unnamed class, or a lambda. //! - //! The key is the *name*, not the address and not a `std::type_index`. - //! `std::type_index` would delegate to `std::type_info::operator==`, - //! and that is only as good as the platform's RTTI uniqueness: - //! libstdc++ falls back to comparing names, but libc++ on Darwin - //! compares uniquely-named RTTI by address. There, two modules' - //! `type_info` objects for the same type compare *unequal* unless the - //! symbol happens to be exported and coalesced by dyld - which, for a - //! template like `method`, requires every - //! template argument to have default visibility as well. Under - //! `-fvisibility=hidden` that is not the case, the copies are not - //! grouped, each module's method keeps its own overrider list, and - //! calls report @ref no_overrider. Comparing names sidesteps the - //! platform's uniqueness rules entirely. + //! @warning Two classes with the same name, local to `static` + //! functions in different translation units, or two instantiations of + //! a template on the addresses of `static` variables with the same + //! name, may be taken for the same class. //! //! @param type A `type_id`. - //! @return The mangled name of the class identified by `type`. - static auto type_index(type_id type) -> std::string_view { - return reinterpret_cast(type)->name(); + //! @return A key identifying the class of `type`. + static auto type_index(type_id type) { + using namespace detail; + + const auto& info = *reinterpret_cast(type); + +#if BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX + return std::type_index(info); +#else + std::string_view name = info.name(); + + return type_key{ + name, itanium_internal_linkage(name) ? type : nullptr + }; +#endif } //! Casts an object to a type. diff --git a/include/boost/openmethod/policies/vptr_map.hpp b/include/boost/openmethod/policies/vptr_map.hpp index a05db22d..d87abc76 100644 --- a/include/boost/openmethod/policies/vptr_map.hpp +++ b/include/boost/openmethod/policies/vptr_map.hpp @@ -111,7 +111,7 @@ class vptr_map : public vptr { //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - return vptr(Registry::rtti::dynamic_type(arg)); + return vptr(detail::dynamic_type_of(arg)); } //! Returns a *reference* to a v-table pointer for a type. diff --git a/include/boost/openmethod/policies/vptr_vector.hpp b/include/boost/openmethod/policies/vptr_vector.hpp index 107938f7..99fd0cf5 100644 --- a/include/boost/openmethod/policies/vptr_vector.hpp +++ b/include/boost/openmethod/policies/vptr_vector.hpp @@ -180,7 +180,7 @@ struct vptr_vector : vptr { //! @return A reference to the v-table pointer for `Class`. template static auto dynamic_vptr(const Class& arg) -> const vptr_type& { - return vptr(Registry::rtti::dynamic_type(arg)); + return vptr(detail::dynamic_type_of(arg)); }; //! Returns a *reference* to a v-table pointer for a type. diff --git a/include/boost/openmethod/preamble.hpp b/include/boost/openmethod/preamble.hpp index 8528ee82..dea42687 100644 --- a/include/boost/openmethod/preamble.hpp +++ b/include/boost/openmethod/preamble.hpp @@ -270,10 +270,21 @@ struct bad_call : openmethod_error { //! The type_id of method that was called type_id method; //! The number of @em virtual arguments in the call + //! + //! The other arguments play no part in dispatch, and are not reported. std::size_t arity; //! The maximum size of `types` static constexpr std::size_t max_types = 16; - //! The type_ids of the arguments. + //! The type_ids of the virtual arguments, in order. + //! + //! `types[i]` describes the i-th virtual argument, whatever its position + //! among all the arguments. Only the first `max_types` are recorded. + //! + //! If the `rtti` policy deems the class polymorphic, the dynamic type of + //! the argument, as reported by the policy. Otherwise, the class whose + //! v-table the argument uses, through a `virtual_ptr` or a v-table pointer + //! stored in the object, or, if that is not conclusive, the class of the + //! parameter. type_id types[max_types]; }; @@ -584,11 +595,35 @@ struct InitializeContext { //! Blueprint for @ref rtti metafunctions (exposition only). template struct RttiFn { - //! Tests if a class is polymorphic. + //! Tests if the policy can find the class of an object at run time. + //! + //! `true` if `Class` is polymorphic in the sense of the policy: given a + //! reference to a `Class`, `dynamic_type` finds the class of the object + //! it refers to. This need not match the C++ notion of a polymorphic + //! class. When it is `true`: + //! + //! @li `virtual_` is a valid parameter type. A class that carries + //! its own v-table pointer, see @ref inplace_vptr_base, is valid either + //! way. + //! + //! @li A @ref virtual_ptr can be constructed from a reference, a pointer + //! or a smart pointer to a `Class`. Otherwise, @ref final_virtual_ptr is + //! the only way to make one. + //! + //! @li With runtime checks, @ref final_virtual_ptr verifies that the class + //! of the object is exactly `Class`. + //! + //! The value is up to the policy, and must be a constant expression. + //! @ref std_rtti uses `std::is_polymorphic_v`; @ref static_rtti + //! deems no class polymorphic. + //! + //! @warning When this is `true`, the policy owes a @ref dynamic_type for + //! `Class`. A policy that deems no class polymorphic may omit + //! `dynamic_type` altogether. //! //! @tparam Class A class. template - static constexpr bool is_polymorphic = std::is_polymorphic_v; + static const bool is_polymorphic; //! Returns the static @ref type_id of a type. //! @@ -603,6 +638,10 @@ struct RttiFn { //! Returns the dynamic @ref type_id of an object. //! + //! Required for every class that @ref is_polymorphic deems polymorphic, + //! and never called for any other, so a policy that deems no class + //! polymorphic may omit it. + //! //! @tparam Class A registered class. //! @param obj A reference to an instance of `Class`. //! @return The type_id of `obj`'s class. @@ -617,11 +656,16 @@ struct RttiFn { template static auto type_name(type_id type, Stream& stream); - //! Returns a key that uniquely identifies a class. + //! Returns a key that identifies a class across modules. + //! + //! The key must compare equal for all the type ids of one class - one per + //! module, typically - and differ for two different classes, including + //! two classes with the same name in different translation units, such as + //! classes defined in anonymous namespaces. It must support `==`, `<` and + //! `std::hash`. //! //! @param type A `type_id`. - //! @return A unique value that identifies a class with the given - //! `type_id`. + //! @return A key that identifies the class of `type`. static auto type_index(type_id type); //! Casts an object to a type. @@ -1122,6 +1166,33 @@ struct initialize_aux; constexpr bool BOOST_PP_CAT(has_, FN) = \ BOOST_PP_CAT(has_, BOOST_PP_CAT(FN, _aux))::value +namespace detail { + +BOOST_OPENMETHOD_DETAIL_HAS_STATIC_FN(dynamic_type); + +// The class of an object, as the registry's `rtti` policy reports it. +// +// A policy owes a `dynamic_type` for every class it deems polymorphic, and +// the library reads the class of an object only through this function, so a +// policy that breaks either half of that contract is named here instead of +// failing somewhere down a dispatch or an error path. +template +auto dynamic_type_of(const Class& obj) -> type_id { + using rtti = typename Registry::rtti; + + static_assert( + rtti::template is_polymorphic>, + "the rtti policy does not deem this class polymorphic"); + static_assert( + has_dynamic_type, + "the rtti policy deems this class polymorphic, so it must provide " + "dynamic_type"); + + return rtti::dynamic_type(obj); +} + +} // namespace detail + //! The single shared instance of a registry's state. //! //! `registry_state` is a thin, function-free class whose only member, `st`, diff --git a/test/implicit_shared_libraries/CMakeLists.txt b/test/implicit_shared_libraries/CMakeLists.txt index e6877e56..b338919f 100644 --- a/test/implicit_shared_libraries/CMakeLists.txt +++ b/test/implicit_shared_libraries/CMakeLists.txt @@ -21,3 +21,4 @@ message(STATUS "Boost.OpenMethod: building implicit shared library tests") add_subdirectory(default_registry) add_subdirectory(custom_registry) +add_subdirectory(named_static_rtti) diff --git a/test/implicit_shared_libraries/Jamfile b/test/implicit_shared_libraries/Jamfile index 0f11d53d..4b378971 100644 --- a/test/implicit_shared_libraries/Jamfile +++ b/test/implicit_shared_libraries/Jamfile @@ -42,3 +42,4 @@ project build-project default_registry ; build-project custom_registry ; +build-project named_static_rtti ; diff --git a/test/implicit_shared_libraries/default_registry/lib.cpp b/test/implicit_shared_libraries/default_registry/lib.cpp index dc331510..f584ad7b 100644 --- a/test/implicit_shared_libraries/default_registry/lib.cpp +++ b/test/implicit_shared_libraries/default_registry/lib.cpp @@ -22,6 +22,16 @@ BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry); BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); +namespace { + +// main.cpp has a Stray of its own, with the same name. They are different +// classes, and must not be merged. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "?"; } @@ -30,6 +40,10 @@ BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "woof"; } +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "library stray"; +} + auto lib_speak_method_type_id() -> const void* { return &typeid( BOOST_OPENMETHOD_TYPE(speak, (virtual_ptr), const char*)); @@ -47,4 +61,8 @@ auto lib_make_dog() -> unique_virtual_ptr { return make_unique_virtual(); } +auto lib_make_stray() -> unique_virtual_ptr { + return make_unique_virtual(); +} + BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/default_registry/lib.hpp b/test/implicit_shared_libraries/default_registry/lib.hpp index d746a9d0..d131820b 100644 --- a/test/implicit_shared_libraries/default_registry/lib.hpp +++ b/test/implicit_shared_libraries/default_registry/lib.hpp @@ -59,4 +59,7 @@ LIB_API auto lib_speak(boost::openmethod::virtual_ptr animal) -> const LIB_API auto lib_make_dog() -> boost::openmethod::unique_virtual_ptr; +// An instance of the library's own Stray, a class in an anonymous namespace. +LIB_API auto lib_make_stray() -> boost::openmethod::unique_virtual_ptr; + #endif diff --git a/test/implicit_shared_libraries/default_registry/main.cpp b/test/implicit_shared_libraries/default_registry/main.cpp index c7a8d2e8..38b34cbb 100644 --- a/test/implicit_shared_libraries/default_registry/main.cpp +++ b/test/implicit_shared_libraries/default_registry/main.cpp @@ -20,11 +20,24 @@ using namespace boost::openmethod; BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); +namespace { + +// Not the library's Stray, although both have the same name. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + // Registered by the executable; the library knows nothing about it. BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { return "meow"; } +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "executable stray"; +} + // The library is linked implicitly, so its static constructors have already run // by the time we get here: a single initialize() sets up dispatch tables that // include both modules' contributions. @@ -52,8 +65,9 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { << (std::type_index(exe_ti) == std::type_index(lib_ti))); // The invariant that matters is the *policy* key: that is what // augment_methods() groups by. std::type_index deliberately is NOT - // asserted - it is false on Darwin, which is the whole reason - // std_rtti::type_index compares names instead. + // asserted - libc++ compares the type_info of a class with hidden + // visibility by address, which is why std_rtti::type_index uses + // std::type_index only with MSVC, and with GCC and libstdc++. bool method_identity_agrees = default_registry::rtti::type_index(exe_id) == default_registry::rtti::type_index(lib_id); @@ -82,4 +96,18 @@ BOOST_AUTO_TEST_CASE(shared_registry_state) { BOOST_TEST(std::string(lib_speak(animal)) == "?"); } +// Two classes in anonymous namespaces, one per module, with the same name. +BOOST_AUTO_TEST_CASE(classes_in_anonymous_namespaces_stay_apart) { + initialize(); + + auto lib_stray = lib_make_stray(); + auto exe_stray = make_unique_virtual(); + + BOOST_TEST(lib_stray.vptr() != exe_stray.vptr()); + BOOST_TEST(std::string(speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(lib_speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(speak(exe_stray)) == "executable stray"); + BOOST_TEST(std::string(lib_speak(exe_stray)) == "executable stray"); +} + BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt b/test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt new file mode 100644 index 00000000..380dd6aa --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright (c) 2017-2026 Jean-Louis Leroy +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt +# or copy at http://www.boost.org/LICENSE_1_0.txt) + +# named_static_rtti shared across an implicitly linked library: the library and +# the executable have different type_ids for the same class, and the registry +# merges them by name. See main.cpp. + +set(lib boost_openmethod-isl_named_static_rtti_lib) +set(exe boost_openmethod-test_implicit_shared_libraries_named_static_rtti) + +add_library(${lib} SHARED lib.cpp) +target_link_libraries(${lib} PRIVATE Boost::openmethod) + +add_executable(${exe} main.cpp) +target_link_libraries(${exe} + PRIVATE Boost::openmethod Boost::unit_test_framework ${lib}) + +# Hidden visibility is the configuration under test - it is b2's default, and +# what BoostRoot.cmake sets. At default visibility, ELF unifies the static +# variables behind the type_ids across modules, and there is nothing to merge. +# Windows DLLs never share them, whatever the setting. +set_target_properties(${lib} ${exe} PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON) + +# See custom_registry/CMakeLists.txt. +set_target_properties(${lib} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY $ + RUNTIME_OUTPUT_DIRECTORY $) + +boost_openmethod_add_test(${exe}) + +if (TARGET tests) + add_dependencies(tests ${exe} ${lib}) +endif() diff --git a/test/implicit_shared_libraries/named_static_rtti/Jamfile b/test/implicit_shared_libraries/named_static_rtti/Jamfile new file mode 100644 index 00000000..91de5f77 --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/Jamfile @@ -0,0 +1,33 @@ +# Boost.OpenMethod Library - implicit_shared_libraries/named_static_rtti Jamfile +# +# Copyright 2017-2026 Jean-Louis Leroy +# +# Distributed under the Boost Software License, Version 1.0. +# See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt + +# named_static_rtti shared across an implicitly linked library: the library and +# the executable have different type_ids for the same class, and the registry +# merges them by name. See main.cpp. +# +# test executable ──linked──▶ lib (owns and exports the registry state) +# +# global is deliberately NOT set: hidden visibility, b2's +# default, is the configuration under test. At global visibility, ELF unifies +# the type_ids across modules, and there is nothing to merge. + +import testing ; + +lib boost_openmethod-isl_named_static_rtti_lib + : lib.cpp + : shared + ; + +run main.cpp + boost_openmethod-isl_named_static_rtti_lib + /boost/test//boost_unit_test_framework/off/static + : + : + : shared + : implicit_shared_libraries_named_static_rtti + ; diff --git a/test/implicit_shared_libraries/named_static_rtti/lib.cpp b/test/implicit_shared_libraries/named_static_rtti/lib.cpp new file mode 100644 index 00000000..6f6648cb --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/lib.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// This library owns and exports the registry state; main.cpp imports it, as in +// the custom_registry variant. +#define OWNS_REGISTRY_STATE +#define LIB_SOURCE + +#include "lib.hpp" +#include "../../test_classes.hpp" + +BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(static_registry); + +using namespace boost::openmethod; + +BOOST_OPENMETHOD_TEST_CLASSES(Object, Animal, Dog, Cat); + +namespace { + +// main.cpp has a Stray of its own, and the compiler prints the same name for +// both. They are different classes, and must not be merged. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + +// A method the executable knows nothing about. Its virtual parameter is a base +// of Animal, so it takes slot 0 in the v-tables of the library's classes, and +// speak takes slot 1. +BOOST_OPENMETHOD(name, (virtual_ptr), const char*); + +BOOST_OPENMETHOD_OVERRIDE(name, (virtual_ptr), const char*) { + return "Object"; +} + +BOOST_OPENMETHOD_OVERRIDE(name, (virtual_ptr), const char*) { + return "Dog"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "?"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "woof"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "library stray"; +} + +auto lib_registry_state_id() -> const void* { + return static_registry::id(); +} + +auto lib_dog_type_id() -> type_id { + return static_registry::rtti::static_type(); +} + +auto lib_speak(virtual_ptr animal) -> const char* { + return speak(animal); +} + +auto lib_make_dog() -> unique_virtual_ptr { + return make_unique_virtual(); +} + +auto lib_make_stray() -> unique_virtual_ptr { + return make_unique_virtual(); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/named_static_rtti/lib.hpp b/test/implicit_shared_libraries/named_static_rtti/lib.hpp new file mode 100644 index 00000000..9d8e9875 --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/lib.hpp @@ -0,0 +1,52 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_LIB_HPP +#define BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_LIB_HPP + +// Defines static_registry, sets it as the default registry, and exports or +// imports its state depending on OWNS_REGISTRY_STATE. Must come before any +// other openmethod header. +#include "registry.hpp" + +#include + +// named_static_rtti treats every class as non-polymorphic, so the classes need +// not be polymorphic. They are visible, as they would have to be under +// std_rtti, but that does not merge their type_ids: those are static variables +// of the library's own templates, which have hidden visibility. +struct BOOST_SYMBOL_VISIBLE Object {}; + +struct BOOST_SYMBOL_VISIBLE Animal : Object {}; + +struct BOOST_SYMBOL_VISIBLE Dog : Animal {}; + +struct BOOST_SYMBOL_VISIBLE Cat : Animal {}; + +BOOST_OPENMETHOD(speak, (boost::openmethod::virtual_ptr), const char*); + +#if defined(LIB_SOURCE) +#define LIB_API BOOST_SYMBOL_EXPORT +#else +#define LIB_API BOOST_SYMBOL_IMPORT +#endif + +// The address identifying the registry's shared state, as seen from inside the +// library. +LIB_API auto lib_registry_state_id() -> const void*; + +// static_type(), as the library computes it. +LIB_API auto lib_dog_type_id() -> boost::openmethod::type_id; + +// Dispatch performed inside the library, on an object created in either module. +LIB_API auto lib_speak(boost::openmethod::virtual_ptr animal) -> const + char*; + +LIB_API auto lib_make_dog() -> boost::openmethod::unique_virtual_ptr; + +// An instance of the library's own Stray, a class in an anonymous namespace. +LIB_API auto lib_make_stray() -> boost::openmethod::unique_virtual_ptr; + +#endif diff --git a/test/implicit_shared_libraries/named_static_rtti/main.cpp b/test/implicit_shared_libraries/named_static_rtti/main.cpp new file mode 100644 index 00000000..9a6c96dd --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/main.cpp @@ -0,0 +1,104 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// named_static_rtti across an implicitly linked shared library built with +// hidden visibility - b2's default, and what BoostRoot.cmake sets - or across a +// Windows DLL. +// +// Like static_rtti, named_static_rtti's type_ids are addresses of static +// variables, and each module has its own copy: the library and the executable +// have different type_ids for the same class. With static_rtti, initialize() +// takes them for unrelated classes, and lays out each module's v-tables on its +// own: a call made in one module, on an object created in the other, lands in +// the wrong overrider, or reads past the end of a v-table. named_static_rtti +// maps the type_ids to the names of the classes, and initialize() merges them. + +#define BOOST_TEST_MODULE implicit_shared_libraries_named_static_rtti + +#include + +#include "lib.hpp" +#include "../../test_classes.hpp" + +#include + +#include + +using namespace boost::openmethod; + +BOOST_OPENMETHOD_TEST_CLASSES(Object, Animal, Dog, Cat); + +namespace { + +// Not the library's Stray, although the compiler prints the same name for both. +// This is the collision that every compiler leaves a mark for; local classes +// are marked only by GCC. +struct Stray : Animal {}; + +} // namespace + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + +// Registered by the executable; the library knows nothing about it. +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "meow"; +} + +BOOST_OPENMETHOD_OVERRIDE(speak, (virtual_ptr), const char*) { + return "executable stray"; +} + +struct initialize_fixture { + initialize_fixture() { + initialize(); + } +}; + +BOOST_TEST_GLOBAL_FIXTURE(initialize_fixture); + +BOOST_AUTO_TEST_CASE(one_class_across_modules) { + BOOST_TEST(lib_registry_state_id() == static_registry::id()); + + // The type_ids differ, which is what named_static_rtti copes with... + auto exe_dog_type_id = static_registry::rtti::static_type(); + BOOST_TEST(lib_dog_type_id() != exe_dog_type_id); + + // ...by mapping them to the same key. + BOOST_TEST( + (static_registry::rtti::type_index(lib_dog_type_id()) == + static_registry::rtti::type_index(exe_dog_type_id))); + + // One class, one v-table, whichever module created the object. + auto lib_dog = lib_make_dog(); + auto exe_dog = make_unique_virtual(); + BOOST_TEST(lib_dog.vptr() == exe_dog.vptr()); +} + +BOOST_AUTO_TEST_CASE(executable_dispatches_on_library_object) { + // The library's speak takes slot 1, behind name, which is declared on a + // base class, and the library lays out the v-tables. If the executable + // had a speak of its own, it would take slot 0, and land in name. + BOOST_TEST(std::string(speak(lib_make_dog())) == "woof"); +} + +BOOST_AUTO_TEST_CASE(library_dispatches_on_executable_object) { + BOOST_TEST(std::string(lib_speak(make_unique_virtual())) == "woof"); + BOOST_TEST(std::string(lib_speak(make_unique_virtual())) == "meow"); + BOOST_TEST(std::string(speak(make_unique_virtual())) == "meow"); + BOOST_TEST(std::string(lib_speak(make_unique_virtual())) == "?"); +} + +BOOST_AUTO_TEST_CASE(classes_in_anonymous_namespaces_stay_apart) { + auto lib_stray = lib_make_stray(); + auto exe_stray = make_unique_virtual(); + + BOOST_TEST(lib_stray.vptr() != exe_stray.vptr()); + BOOST_TEST(std::string(speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(lib_speak(lib_stray)) == "library stray"); + BOOST_TEST(std::string(speak(exe_stray)) == "executable stray"); + BOOST_TEST(std::string(lib_speak(exe_stray)) == "executable stray"); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/implicit_shared_libraries/named_static_rtti/registry.hpp b/test/implicit_shared_libraries/named_static_rtti/registry.hpp new file mode 100644 index 00000000..0d7ae607 --- /dev/null +++ b/test/implicit_shared_libraries/named_static_rtti/registry.hpp @@ -0,0 +1,28 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_REGISTRY_HPP +#define BOOST_OPENMETHOD_TEST_IMPLICIT_SHARED_LIBRARIES_NAMED_STATIC_RTTI_REGISTRY_HPP + +#include + +struct static_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY static_registry + +#include +#include + +struct BOOST_SYMBOL_VISIBLE static_registry : + boost::openmethod::registry< + boost::openmethod::policies::named_static_rtti> {}; + +// The registry state is shared exactly as in the custom_registry variant. +#if defined(OWNS_REGISTRY_STATE) +BOOST_OPENMETHOD_EXPORT_REGISTRY(static_registry); +#else +BOOST_OPENMETHOD_IMPORT_REGISTRY(static_registry); +#endif + +#endif diff --git a/test/test_custom_rtti_deferred.cpp b/test/test_custom_rtti_deferred.cpp index 1d52087f..d818c058 100644 --- a/test/test_custom_rtti_deferred.cpp +++ b/test/test_custom_rtti_deferred.cpp @@ -122,12 +122,9 @@ struct custom_rtti : boost::openmethod::policies::deferred_static_rtti { template static auto dynamic_type(const T& obj) { - if constexpr (is_polymorphic) { - return boost::openmethod::type_id(obj.type); - } else { - return boost::openmethod::type_id( - non_polymorphic_static_type()); - } + static_assert(is_polymorphic); + + return boost::openmethod::type_id(obj.type); } template diff --git a/test/test_custom_rtti_simple.cpp b/test/test_custom_rtti_simple.cpp index 712ab871..9c4e2328 100644 --- a/test/test_custom_rtti_simple.cpp +++ b/test/test_custom_rtti_simple.cpp @@ -73,12 +73,9 @@ struct custom_rtti : boost::openmethod::policies::rtti { template static auto dynamic_type(const T& obj) { - if constexpr (is_polymorphic) { - return boost::openmethod::type_id(obj.type); - } else { - return boost::openmethod::type_id( - non_polymorphic_static_type()); - } + static_assert(is_polymorphic); + + return boost::openmethod::type_id(obj.type); } template diff --git a/test/test_custom_rtti_simple_projection.cpp b/test/test_custom_rtti_simple_projection.cpp index 45a30da9..15930b9c 100644 --- a/test/test_custom_rtti_simple_projection.cpp +++ b/test/test_custom_rtti_simple_projection.cpp @@ -58,12 +58,9 @@ struct custom_rtti : boost::openmethod::policies::rtti { template static auto dynamic_type(const T& obj) { - if constexpr (is_polymorphic) { - return obj.type; - } else { - return static_cast( - &non_polymorphic_static_type_storage); - } + static_assert(is_polymorphic); + + return obj.type; } template diff --git a/test/test_custom_rtti_virtual_base.cpp b/test/test_custom_rtti_virtual_base.cpp index 23a1afc3..2c259d9e 100644 --- a/test/test_custom_rtti_virtual_base.cpp +++ b/test/test_custom_rtti_virtual_base.cpp @@ -94,12 +94,9 @@ struct custom_rtti : boost::openmethod::policies::rtti { template static auto dynamic_type(const T& obj) { - if constexpr (is_polymorphic) { - return boost::openmethod::type_id(obj.type); - } else { - return boost::openmethod::type_id( - non_polymorphic_static_type()); - } + static_assert(is_polymorphic); + + return boost::openmethod::type_id(obj.type); } template diff --git a/test/test_dispatch_std_any.cpp b/test/test_dispatch_std_any.cpp index 32463be5..ac54b5ef 100644 --- a/test/test_dispatch_std_any.cpp +++ b/test/test_dispatch_std_any.cpp @@ -433,4 +433,53 @@ BOOST_AUTO_TEST_CASE(std_any_class_in_hierarchy) { } } // namespace BOOST_OPENMETHOD_GENSYM +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a bad_call reports the class held in the `any`, not `std::any` + +struct throw_registry : + default_registry::with {}; + +struct Dog {}; + +struct Cat {}; + +using name_method = method< + struct name_id, std::string(virtual_), throw_registry>; + +using pet_method = method< + struct pet_id, std::string(virtual_), throw_registry>; + +auto name_dog(const Dog&) -> std::string { + return "dog"; +} + +auto pet_cat(const Cat&) -> std::string { + return "purr"; +} + +// `name_dog` registers Dog; `pet` has no overrider for it. +BOOST_OPENMETHOD_REGISTER(name_method::override); +BOOST_OPENMETHOD_REGISTER(pet_method::override); + +BOOST_AUTO_TEST_CASE(std_any_bad_call_types) { + using rtti = throw_registry::rtti; + + initialize(); + + try { + pet_method::fn(std::any(Dog())); + BOOST_FAIL("should have thrown"); + } catch (const no_overrider& error) { + BOOST_TEST(error.arity == 1u); + BOOST_TEST( + (rtti::type_index(error.types[0]) == + rtti::type_index(rtti::static_type()))); + } catch (...) { + BOOST_FAIL("wrong exception"); + } +} +} // namespace BOOST_OPENMETHOD_GENSYM + BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_initialize_dropped_class.cpp b/test/test_initialize_dropped_class.cpp index 4a61c4ed..eb090415 100644 --- a/test/test_initialize_dropped_class.cpp +++ b/test/test_initialize_dropped_class.cpp @@ -105,11 +105,9 @@ struct small_rtti : policies::rtti { template static auto dynamic_type(const T& obj) -> type_id { - if constexpr (is_polymorphic) { - return type_id(obj.type); - } else { - return type_id(other_static_type()); - } + static_assert(is_polymorphic); + + return type_id(obj.type); } }; }; diff --git a/test/test_named_static_rtti.cpp b/test/test_named_static_rtti.cpp new file mode 100644 index 00000000..ece4f9fc --- /dev/null +++ b/test/test_named_static_rtti.cpp @@ -0,0 +1,161 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +struct named_registry; +#define BOOST_OPENMETHOD_DEFAULT_REGISTRY named_registry + +#include +#include +#include +#include + +struct named_registry : + boost::openmethod::registry< + boost::openmethod::policies::named_static_rtti> {}; + +#define BOOST_TEST_MODULE openmethod +#include + +#include "test_classes.hpp" + +#include +#include +#include + +struct Animal {}; + +struct Dog : Animal {}; + +struct Cat : Animal {}; + +namespace { + +struct Stray : Animal {}; + +} // namespace + +using namespace boost::openmethod::aliases; + +BOOST_OPENMETHOD_TEST_CLASSES(Animal, Dog, Cat); + +BOOST_OPENMETHOD_CLASSES(Animal, Stray); + +BOOST_OPENMETHOD(poke, (virtual_ptr, std::ostream&), void); + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr, std::ostream& os), void) { + os << "bark"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr, std::ostream& os), void) { + os << "hiss"; +} + +BOOST_OPENMETHOD_OVERRIDE(poke, (virtual_ptr, std::ostream& os), void) { + os << "growl"; +} + +using rtti = named_registry::rtti; + +template +auto key() { + return rtti::type_index(rtti::static_type()); +} + +template +auto name() -> std::string { + std::ostringstream os; + rtti::type_name(rtti::static_type(), os); + + return os.str(); +} + +BOOST_AUTO_TEST_CASE(names) { + // The class-keys MSVC prints are dropped, so the name is the same on + // every compiler. + BOOST_TEST(name() == "Dog"); + BOOST_TEST(name() == "Cat"); +} + +// MSVC prints the class-key of every class it names; the other compilers +// print none. Only the strip itself can be checked here, on names spelled the +// way MSVC spells them. +BOOST_AUTO_TEST_CASE(class_keys) { + using boost::openmethod::detail::static_rtti_strip; + using boost::openmethod::detail::static_rtti_stripped_size; + +#define TEST_STRIP(RAW, STRIPPED) \ + { \ + constexpr std::string_view raw = RAW; \ + constexpr auto size = static_rtti_stripped_size(raw); \ + constexpr auto chars = static_rtti_strip(raw); \ + BOOST_TEST(std::string_view(chars.data(), size) == STRIPPED); \ + } + + TEST_STRIP("struct Dog", "Dog") + TEST_STRIP("class Dog", "Dog") + TEST_STRIP("union U", "U") + TEST_STRIP("Dog", "Dog") + TEST_STRIP("struct Box", "Box") + TEST_STRIP("struct A>", "A>") + TEST_STRIP("ns::class Y", "ns::Y") + TEST_STRIP( + "`anonymous-namespace'::struct Stray", "`anonymous-namespace'::Stray") + + // A class-key is only one where a name cannot continue. + TEST_STRIP("structural", "structural") + TEST_STRIP("enumerator", "enumerator") + TEST_STRIP("struct Box", "Box") + TEST_STRIP("myclass X", "myclass X") + +#undef TEST_STRIP +} + +BOOST_AUTO_TEST_CASE(keys) { + BOOST_TEST((key() == key())); + BOOST_TEST((key() != key())); + BOOST_TEST((key() < key() || key() < key())); + + // A class at namespace scope is identified by its name alone... + BOOST_TEST(key().local == nullptr); + + // ...one in an anonymous namespace by its type_id as well, on every + // compiler. + BOOST_TEST(key().local == rtti::static_type()); +} + +// GCC prints both classes as `local_classes::test_method()::Local`, and marks +// them as local; reflection does not need marks. Clang and MSVC leave no mark: +// see the warning in the documentation of named_static_rtti. +#if BOOST_OPENMETHOD_HAS_REFLECTION || defined(BOOST_GCC) +BOOST_AUTO_TEST_CASE(local_classes) { + struct Local {}; + auto first = key(); + + { + struct Local {}; + auto second = key(); + + BOOST_TEST(first.local != nullptr); + BOOST_TEST((first != second)); + } +} +#endif + +BOOST_AUTO_TEST_CASE(dispatch) { + boost::openmethod::initialize(); + + auto poked = [](auto animal) { + std::ostringstream os; + poke(animal, os); + + return os.str(); + }; + + BOOST_TEST(poked(make_unique_virtual()) == "bark"); + BOOST_TEST(poked(make_unique_virtual()) == "hiss"); + BOOST_TEST(poked(make_unique_virtual()) == "growl"); +} + +BOOST_OPENMETHOD_TEST_REGISTER_CLASSES(); diff --git a/test/test_slot_allocator.cpp b/test/test_slot_allocator.cpp index 55feef8f..0e4e36ea 100644 --- a/test/test_slot_allocator.cpp +++ b/test/test_slot_allocator.cpp @@ -70,8 +70,9 @@ auto sstr(const std::unordered_set& container) { template auto get_class(const Compiler& comp) { // Key through the rtti policy rather than with a raw typeid: the policy - // decides what identifies a class (std_rtti uses the mangled name, see - // std_rtti::type_index), and only it knows how to derive that from a type. + // decides what identifies a class (see std_rtti::type_index, whose key + // type depends on the platform), and only it knows how to derive that + // from a type. return comp.class_map.at( boost::openmethod::default_registry::rtti::type_index(&typeid(T))); } diff --git a/test/test_static_rtti_errors.cpp b/test/test_static_rtti_errors.cpp new file mode 100644 index 00000000..1a0d9c97 --- /dev/null +++ b/test/test_static_rtti_errors.cpp @@ -0,0 +1,167 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// static_rtti and named_static_rtti have no dynamic_type. A bad_call still +// reports the class of each argument: the one whose v-table the virtual_ptr +// points to. + +#include +#include +#include +#include +#include +#include +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include + +#include +#include +#include + +using namespace boost::openmethod; + +struct static_registry : + registry {}; + +struct named_registry : + registry {}; + +struct Animal {}; + +struct Dog : Animal {}; + +struct Cat : Animal {}; + +struct BOOST_OPENMETHOD_ID(speak); +struct BOOST_OPENMETHOD_ID(meet); +struct BOOST_OPENMETHOD_ID(sniff); + +template +void speak_cat(virtual_ptr) { +} + +template +void sniff_cat(int, virtual_ptr) { +} + +template +void meet_dog_animal( + virtual_ptr, virtual_ptr) { +} + +template +void meet_animal_dog( + virtual_ptr, virtual_ptr) { +} + +// Checks that `call` reports `Error`, and that every argument is reported as a +// Dog - the class of the object, not Animal, the class of the parameter. +template +void check_error(Call call, std::size_t arity) { + using rtti = typename Registry::rtti; + + try { + call(); + BOOST_FAIL("should have thrown"); + } catch (const Error& error) { + BOOST_TEST(error.arity == arity); + + for (std::size_t i = 0; i < arity; ++i) { + BOOST_TEST( + (rtti::type_index(error.types[i]) == + rtti::type_index(rtti::template static_type()))); + + if constexpr (std::is_same_v) { + std::ostringstream os; + rtti::type_name(error.types[i], os); + BOOST_TEST(os.str() == "Dog"); + } + } + } catch (...) { + BOOST_FAIL("wrong exception"); + } +} + +using registries = boost::mp11::mp_list; + +BOOST_AUTO_TEST_CASE_TEMPLATE(bad_call_types, Registry, registries) { + BOOST_OPENMETHOD_REGISTER(use_classes); + + using speak = method< + BOOST_OPENMETHOD_ID(speak), void(virtual_ptr), + Registry>; + BOOST_OPENMETHOD_REGISTER( + typename speak::template override>); + + using meet = method< + BOOST_OPENMETHOD_ID(meet), + void(virtual_ptr, virtual_ptr), + Registry>; + BOOST_OPENMETHOD_REGISTER( + typename meet::template override< + meet_dog_animal, meet_animal_dog>); + + using sniff = method< + BOOST_OPENMETHOD_ID(sniff), void(int, virtual_ptr), + Registry>; + BOOST_OPENMETHOD_REGISTER( + typename sniff::template override>); + + initialize(); + Dog dog; + + check_error( + [&] { speak::fn(final_virtual_ptr(dog)); }, 1); + check_error( + [&] { speak::fn(make_unique_virtual()); }, 1); + check_error( + [&] { + meet::fn( + final_virtual_ptr(dog), + final_virtual_ptr(dog)); + }, + 2); + + // A non-virtual argument plays no part in dispatch, and is not reported: + // the arity counts the virtual arguments only, and `types[0]` is the first + // of them, though it is the second argument. + check_error( + [&] { sniff::fn(42, final_virtual_ptr(dog)); }, 1); +} + +// A class that carries its own v-table pointer needs no dynamic_type: the +// class is read from the pointer. The classes must not be polymorphic in the +// C++ sense: inplace_vptr_base's constructor casts `this` to the derived +// class, which UBSan rejects while the C++ v-table pointer is not set yet. +struct inplace_registry : + registry {}; + +struct Shape : inplace_vptr_base {}; + +struct Square : Shape, inplace_vptr_derived {}; + +BOOST_OPENMETHOD_CLASSES(Shape, Square, inplace_registry); + +BOOST_OPENMETHOD(draw, (virtual_), void, inplace_registry); + +BOOST_AUTO_TEST_CASE(bad_call_types_inplace_vptr) { + using rtti = inplace_registry::rtti; + + initialize(); + Square square; + + try { + draw(square); + BOOST_FAIL("should have thrown"); + } catch (const no_overrider& error) { + BOOST_TEST(error.types[0] == rtti::static_type()); + } catch (...) { + BOOST_FAIL("wrong exception"); + } +} diff --git a/test/test_std_rtti_type_index.cpp b/test/test_std_rtti_type_index.cpp new file mode 100644 index 00000000..f41670d2 --- /dev/null +++ b/test/test_std_rtti_type_index.cpp @@ -0,0 +1,97 @@ +// Copyright (c) 2017-2026 Jean-Louis Leroy +// Distributed under the Boost Software License, Version 1.0. +// See accompanying file LICENSE_1_0.txt +// or copy at http://www.boost.org/LICENSE_1_0.txt) + +// std_rtti::type_index must be equal for the copies of a class in different +// modules, and different for two classes with the same name in anonymous +// namespaces, or unnamed. Where std::type_index does not do that, it looks for +// their marks in the mangled name. + +#include + +#define BOOST_TEST_MODULE openmethod +#include + +#include + +using boost::openmethod::default_registry; +using boost::openmethod::detail::itanium_internal_linkage; + +// Names produced by GCC, Clang, and Clang with libc++. +BOOST_AUTO_TEST_CASE(mangled_names) { + const char* shared[] = { + "6Global", + "N2ns5InnerE", + "10ZLibStream", + "12UtilityClass", + "8Foo$Impl", // `$` is a valid identifier character on GCC and Clang + "6A$B$C$", + "4A$_B", // `$_` is Clang's mark only when a digit follows + "N3app3BoxINS_1XENS_7UtilityEE", // `Ut` after a substitution + "3BoxISt3mapINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESt6" + "vectorIiSaIiEESt4lessIS6_ESaISt4pairIKS6_S9_EEEE", + "3BoxINSt3__13mapINS0_12basic_stringIcNS0_11char_traitsIcEENS0_" + "9allocatorIcEEEENS0_6vectorIiNS5_IiEEEENS0_4lessIS7_EENS5_INS0_" + "4pairIKS7_SA_EEEEEEE", + "4VBoxIL5Color12EE", + "4VBoxILN2ns5ShadeE12EE", + "4VBoxILin3EE", + "4VBoxIXadL_Z10extern_varEEE", + "4VBoxIXadL_Z10extern_varEEL5Color1EE", // an enum literal after `_Z` + "3BoxIA10_iE", + "Z15local_in_externvE5Local", + "N5boost10openmethod6methodI22speak_boost_openmethodFvNS0_11virtual_" + "ptrI6GlobalNS0_16default_registryEvEESt3mapINSt7__cxx1112basic_" + "stringIcSt11char_traitsIcESaIcEEESt6vectorIiSaIiEESt4lessISD_ESaISt4" + "pairIKSD_SG_EEEES5_EE", + }; + + for (auto name : shared) { + BOOST_TEST_INFO(name); + BOOST_TEST(!itanium_internal_linkage(name)); + } + + const char* internal[] = { + "N12_GLOBAL__N_14AnonE", + "3BoxIN12_GLOBAL__N_14AnonEE", + "N3app3BoxINS_12_GLOBAL__N_11YEEE", // after a substitution + "4VBoxILN2ns5ShadeE12EN12_GLOBAL__N_11XEE", // after an enum literal + "3$_0", // Clang: an unnamed type + "10._anon_100", // GCC: an unnamed type + }; + + for (auto name : internal) { + BOOST_TEST_INFO(name); + BOOST_TEST(itanium_internal_linkage(name)); + } +} + +struct Global {}; + +struct Other {}; + +namespace { +struct Anon {}; +} // namespace + +template +auto key() { + return default_registry::rtti::type_index( + default_registry::rtti::static_type()); +} + +BOOST_AUTO_TEST_CASE(keys) { + BOOST_TEST((key() == key())); + BOOST_TEST((key() != key())); + BOOST_TEST((key() < key() || key() < key())); + +#if !BOOST_OPENMETHOD_DETAIL_STD_TYPE_INDEX + // The scanner is in use: check it on the names of this compiler. + BOOST_TEST(key().local == nullptr); + BOOST_TEST( + key().local == default_registry::rtti::static_type()); + BOOST_TEST(!itanium_internal_linkage(typeid(Global).name())); + BOOST_TEST(itanium_internal_linkage(typeid(Anon).name())); +#endif +} diff --git a/test/test_virtual_any_std.cpp b/test/test_virtual_any_std.cpp index 2543cd3d..e457a63f 100644 --- a/test/test_virtual_any_std.cpp +++ b/test/test_virtual_any_std.cpp @@ -341,4 +341,56 @@ BOOST_AUTO_TEST_CASE(virtual_any_mixed_with_virtual_ptr) { } } // namespace BOOST_OPENMETHOD_GENSYM +namespace BOOST_OPENMETHOD_GENSYM { + +// ----------------------------------------------------------------------------- +// a bad_call reports the class held in the virtual_any, not the virtual_any + +struct throw_registry : + default_registry::with {}; + +struct Dog {}; + +struct Cat {}; + +using throw_any = virtual_any; + +using name_method = + method; + +using pet_method = + method; + +auto name_dog(const Dog&) -> std::string { + return "dog"; +} + +auto pet_cat(const Cat&) -> std::string { + return "purr"; +} + +// `name_dog` registers Dog; `pet` has no overrider for it. +BOOST_OPENMETHOD_REGISTER(name_method::override); +BOOST_OPENMETHOD_REGISTER(pet_method::override); + +BOOST_AUTO_TEST_CASE(virtual_any_bad_call_types) { + using rtti = throw_registry::rtti; + + initialize(); + throw_any dog = Dog(); + + try { + pet_method::fn(dog); + BOOST_FAIL("should have thrown"); + } catch (const no_overrider& error) { + BOOST_TEST(error.arity == 1u); + BOOST_TEST( + (rtti::type_index(error.types[0]) == + rtti::type_index(rtti::static_type()))); + } catch (...) { + BOOST_FAIL("wrong exception"); + } +} +} // namespace BOOST_OPENMETHOD_GENSYM + BOOST_OPENMETHOD_TEST_REGISTER_CLASSES();