7

I stumbled about an issue while using libstdc++'s std::any implementation with mingw across a shared library boundary. It produces a std::bad_any_cast where it obviously should not (i believe).

I use mingw-w64, gcc-7 and compile the code with -std=c++1z.

The simplified code:

main.cpp:

#include <any>
#include <string>

// prototype from lib.cpp
void do_stuff_with_any(const std::any& obj);

int main()
{
    do_stuff_with_any(std::string{"Hello World"});
}

lib.cpp:

Will be compiled into a shared library and linked with the executable from main.cpp.

#include <any>
#include <iostream>

void do_stuff_with_any(const std::any& obj)
{
    std::cout << std::any_cast<const std::string&>(obj) << "\n";
}

This triggers a std::bad_any_cast although the any passed to do_stuff_with_any does contain a string. I digged into gcc's any implementation and it seems to use comparison of the address of a static inline member function (a manager chosen from a template struct depending on the type of the stored object) to check if the any holds an object of the requested type. And the address of this function seems to change across the shared library boundary.

Isn't std::any guaranteed to work across shared library boundaries? Does this code trigger UB somewhere? Or is this a bug in the gcc implementation? I am pretty sure it works on linux so is this only a bug in mingw? Is it known or should i report it somewhere if so? Any ideas for (temporary) workarounds?

Billal Begueradj
  • 20,717
  • 43
  • 112
  • 130
nyorain
  • 71
  • 3
  • Relevant, possibly the answer: https://stackoverflow.com/q/44468900/2069064 – Barry Jul 24 '17 at 21:40
  • The linked answer says that when simply linking the shared library (i am not using dlopen or similar), no additional steps are needed. Seems not to be the case here. – nyorain Jul 24 '17 at 22:05
  • 2
    @Barry This is different. https://softwareengineering.stackexchange.com/a/176690/102229 about covers it? – T.C. Jul 25 '17 at 00:16
  • yes, seems like this is the problem. Thank you – nyorain Jul 25 '17 at 07:10

1 Answers1

4

While it is true that this is an issue on how Windows DLLs work, and that as of GCC 8.2.0, the issue still remains, this can be easily worked around by changing the __any_caster function inside the any header to this:

template<typename _Tp>
void* __any_caster(const any* __any)
{
  if constexpr (is_copy_constructible_v<decay_t<_Tp>>)
{
#if __cpp_rtti
  if (__any->type().hash_code() == typeid(_Tp).hash_code())
#else
  if (__any->_M_manager == &any::_Manager<decay_t<_Tp>>::_S_manage)
#endif
    {
      any::_Arg __arg;
      __any->_M_manager(any::_Op_access, __any, &__arg);
      return __arg._M_obj;
    }
}
  return nullptr;
}

Or something similar, the only relevant part is the comparison line wrapped in the #if.

To elaborate, there is 2 copies of the manager function one on the exe and one on the dll, the passed object contains the address of the exe because that's where it was created, but once it reaches the dll side, the pointer gets compared to the one in the dll address space, which will never match, so, instead type info hash_codes should be compared instead.

  • Have you tested this? Or why doesn't the typeid() also have the same issue? – Fabio A. Jan 02 '21 at 07:41
  • I did, but I found a workaround that does not require changing system headers, so I am not using this. typeid works because it is a keyword, not a template. – Rodrigo Hernandez Jan 03 '21 at 20:52
  • @RodrigoHernandez, could you post your workaround as an answer? – Benjamin Bannier Jan 14 '21 at 14:09
  • The workaround was that I had to implement my own [unique-pointer-to-anything class](https://github.com/AeonGames/AeonEngine/blob/master/include/aeongames/UniqueAnyPtr.h) The workaround itself is the same as this answer, use the hash_code to determine the type of the pointer: template bool HasType() const { return GetTypeInfo().hash_code() == typeid ( T ).hash_code(); } – Rodrigo Hernandez Apr 25 '22 at 21:29