Two-level namespace and coalesced typeinfos

As you may know libc++ on MacOS is using weird implementation of RTTI: it compares typeinfo objects by pointers instead of comparing name-strings (like it happens in standard runtimes on Windows and Linux). Since each module has its own typeinfo instance - dynamic_cast for polymorphic types and exception handling may not work across module boundaries if you use -fvisibility=hidden and/or two-level namespace (At the runtime you will either get error from dynamic_cast, or fail to catch exception that was thrown from another module). The problem is that sometimes we have ODR violations because of legacy code, and sometimes there are reasons to legally violate ODR and define same typeinfo in multiple modules - templated polymorphic type. To make polymorphic types work across modules we must make sure that our typeinfo symbols are coalesced (merged) across these modules.

This is why I started investigating the way symbols are coalesced on MacOS, my observations so far:

  • For dylib compiled with -flat_namespace everything seems to work as in linux - strong definition in our dylib is overridden by strong definition from another dylib only if another dylib is loaded before ours and both definitions are visible, another library might be compiled with two-level namespace. Weak definitions can be overridden by other weak definitions or strong definition.
  • For dylib compiled with two-level namespace (default and recommended mode) strong symbol definitions cannot be overridden, so there is no way to merge strong typeinfos (these are normally defined for non-templated polymorphic classes). At the same time typeinfo of templated is represented as weak definition, which gets overridden by any previous definition of typeinfo in process, if another dylib with strong/weak visible definition is loaded before our dylib.

Questions (sorry for 3 questions at once, but I am really confused):

  1. Is there any way to override/coalesce strong symbol definition when this definition is in dylib built with two-level namespace? In the example below this would mean calling sharedlib::Print of main.cpp from libsharedlib.dylib. This works in flat_namespace mode, but not in two-level namespace. Maybe I am missing something.
  2. What is the point of two level namespace if weak definitions of my dylib can be overridden by any bad implementation from customer's process? I thought two-level namespace was supposed to protect my dylib from picking up unexpected symbol definitions (for example from another boost version used in process of my customer), but it seems this expectation is not holding up for templated polymorphic classes, and for all weak symbols in general.
  3. Is there any common way to handle issue with multiple typeinfos on MacOS? I think one of solutions would be to use -fvisibility-ms-compat and -flat_namespace, which is basically -fvisibility=hidden + it makes all vtables and typeinfos visible, -flat_namespace makes sure all typeinfos for the same types are merged into one by dynamic linker. The problem with this approach is that it seems hacky, and it also makes typeinfo and vtable of some boost header-only classes to be visible in our dylib. I am afraid we can break customer's process if at some point these boost classes change order of methods, or change set of methods (therefore vtable will become incompatible).

If you want to play with example, here it is:

templates.hpp:

#pragma once

namespace Foo
{
template<typename T>
class Bar
{
public:
    Bar(){};
    virtual ~Bar(){};
    virtual void SetValue(const T& v) { m_value = v; }
    T m_value;
};
}

main.cpp:

#include <typeinfo>
#include <cstdio>
#include "templates.hpp"

namespace sharedlib
{
    void CheckTemplate(const Foo::Bar<double>& b);
    void __attribute__ ((visibility ("default"))) Print()
    {
        printf("Print from main executable\n");
    }
}

int main(void)
{
    Foo::Bar<double> b;
    sharedlib::CheckTemplate(b);
    return 0;
}

sharedlib.cpp:

#include "templates.hpp"
#include <typeinfo>
#include <cstdio>

namespace sharedlib
{
    void __attribute__ ((visibility ("default"))) Print()
    {
        printf("Print from sharedlib\n");
    }

    void __attribute__ ((visibility ("default"))) CheckTemplate(const Foo::Bar<double>& b)
    {
        printf("&typeinfo=%p\n", static_cast<const void*>(&typeid(b)));
        Foo::Bar<double> localB;
        printf("&typeinfo=%p\n", static_cast<const void*>(&typeid(localB)));

        Print();
    }
}

Makefile:

check:
	./main

all: main sharedlib

main.o: main.cpp
	clang++ -fvisibility-ms-compat -c main.cpp

sharedlib.o: sharedlib.cpp
	clang++ -fvisibility-ms-compat -c sharedlib.cpp

sharedlib.dylib: sharedlib.o
	clang++ sharedlib.o -dynamiclib -o libsharedlib.dylib
	# If you use flat_namespace, sharedlib::Print will be replaced by sharedlib::Print defined in main.cpp
    #clang++ sharedlib.o -dynamiclib -Wl,-flat_namespace -o libsharedlib.dylib

main: main.o sharedlib.dylib
	clang++ main.o -L. -lsharedlib -o main

clean:
	rm -rf *.o *.dylib main

Some commands to check results:

# check if TWO_LEVEL namespace is used by the dylib
otool -Vh ./sharedlib.dylib  
# check all symbols
nm -om ./libsharedlib.dylib | c++filt
# output:
...
./libsharedlib.dylib:                  (undefined) external std::terminate() (from libc++)
./libsharedlib.dylib: 0000000000004040 (__DATA_CONST,__const) weak external typeinfo for Foo::Bar<double>
./libsharedlib.dylib: 0000000000003f70 (__TEXT,__const) weak external typeinfo name for Foo::Bar<double>
...
# ld verbose mode, to see how coalescing happens:
DYLD_PRINT_BINDINGS=1 ./main
Two-level namespace and coalesced typeinfos
 
 
Q