Использование clang ++, -fvisibility = hidden, typeinfo и стирание типов

Это уменьшенная версия проблемы, с которой я сталкиваюсь в clang ++ на Mac OS X. Она была серьезно отредактирована, чтобы лучше отразить истинную проблему (первая попытка описать проблему не была связана с проблемой).

У меня есть этот большой кусок программного обеспечения на C ++ с большим набором символов в объектных файлах, поэтому я использую -fvisibility=hidden чтобы мои таблицы символов были маленькими. Общеизвестно, что в таком случае нужно уделять дополнительное внимание vtables, и я полагаю, я столкнулся с этой проблемой. Однако я не знаю, как изящно решить эту проблему так, чтобы это понравилось и gcc, и clang.

Рассмотрим base класс, в котором есть оператор приведения, asи derived шаблон класса, который содержит некоторую полезную нагрузку. Пара base/derived<T> используется для реализации стирания типа:

// foo.hh

#define API __attribute__((visibility("default")))

struct API base
{
virtual ~base() {}

template <typename T>
const T& as() const
{
return dynamic_cast<const T&>(*this);
}
};

template <typename T>
struct API derived: base
{};

struct payload {}; // *not* flagged as "default visibility".

API void bar(const base& b);
API void baz(const base& b);

Затем у меня есть два разных модуля компиляции, которые предоставляют похожую услугу, которую я могу приблизительно удвоить одной и той же функцией: base в derive<payload>:

// bar.cc
#include "foo.hh"void bar(const base& b)
{
b.as<derived<payload>>();
}

а также

// baz.cc
#include "foo.hh"void baz(const base& b)
{
b.as<derived<payload>>();
}

Из этих двух файлов я создаю dylib. Здесь main функция, вызывающая эти функции из dylib:

// main.cc
#include <stdexcept>
#include <iostream>
#include "foo.hh"
int main()
try
{
derived<payload> d;
bar(d);
baz(d);
}
catch (std::exception& e)
{
std::cerr << e.what() << std::endl;
}

Наконец, Makefile для компиляции и связывания всех. Здесь нет ничего особенного, кроме, конечно, -fvisibility=hidden,

CXX = clang++
CXXFLAGS = -std=c++11 -fvisibility=hidden

all: main

main: main.o bar.dylib baz.dylib
$(CXX) -o $@ $^

%.dylib: %.cc foo.hh
$(CXX) $(CXXFLAGS) -shared -o $@ $<

%.o: %.cc foo.hh
$(CXX) $(CXXFLAGS) -c -o $@ $<

clean:
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib

Выполнение успешно с gcc (4.8) на OS X:

$ make clean && make CXX=g++-mp-4.8 && ./main
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
g++-mp-4.8 -std=c++11 -fvisibility=hidden -c main.cc -o main.o
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
g++-mp-4.8 -o main main.o bar.dylib baz.dylib

Однако с помощью clang (3.4) это не получается:

$ make clean && make CXX=clang++-mp-3.4 && ./main
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -c main.cc -o main.o
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
clang++-mp-3.4 -o main main.o bar.dylib baz.dylib
std::bad_cast

Однако это работает, если я использую

struct API payload {};

но я не хочу выставлять тип полезной нагрузки. Итак, мои вопросы:

  1. почему GCC и Clang здесь разные?
  2. это действительно работа с GCC, или мне просто «повезло» в использовании неопределенного поведения?
  3. у меня есть средства, чтобы избежать payload выйти на публику с Clang ++?

Заранее спасибо.

Теперь я лучше понимаю, что происходит. Похоже, что оба GCC а также clang требует, чтобы и шаблон класса, и его параметр были видны (в смысле ELF) для создания уникального типа. Если вы измените bar.cc а также baz.cc функционирует следующим образом:

// bar.cc
#include "foo.hh"void bar(const base& b)
{
std::cerr
<< "bar value: " << &typeid(b) << std::endl
<< "bar type:  " << &typeid(derived<payload>) << std::endl
<< "bar equal: " << (typeid(b) == typeid(derived<payload>)) << std::endl;
b.as<derived<payload>>();
}

а также если ты делаешь payload видимо тоже:

struct API payload {};

тогда вы увидите, что и GCC, и Clang преуспеют:

$ make clean && make CXX=g++-mp-4.8
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
g++-mp-4.8 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
./g++-mp-4.8 -o main main.o bar.dylib baz.dylib
$ ./main
bar value: 0x106785140
bar type:  0x106785140
bar equal: 1
baz value: 0x106785140
baz type:  0x106785140
baz equal: 1

$ make clean && make CXX=clang++-mp-3.4
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
clang++-mp-3.4 -o main main.o bar.dylib baz.dylib
$ ./main
bar value: 0x10a6d5110
bar type:  0x10a6d5110
bar equal: 1
baz value: 0x10a6d5110
baz type:  0x10a6d5110
baz equal: 1

Равенство типов легко проверить, на самом деле существует единственный экземпляр типа, о чем свидетельствует его уникальный адрес.

Однако, если вы удалите видимый атрибут из payload:

struct payload {};

тогда вы получите с GCC:

$ make clean && make CXX=g++-mp-4.8
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
g++-mp-4.8 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
g++-mp-4.8 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
g++-mp-4.8 -o main main.o bar.dylib baz.dylib
$ ./main
bar value: 0x10faea120
bar type:  0x10faf1090
bar equal: 1
baz value: 0x10faea120
baz type:  0x10fafb090
baz equal: 1

Сейчас есть несколько экземпляров типа derived<payload> (о чем свидетельствуют три разных адреса), но GCC видит, что эти типы равны, и (конечно) два dynamic_cast проходить.

В случае clang все по-другому:

$ make clean && make CXX=clang++-mp-3.4
rm -f main main.o bar.o baz.o bar.dylib baz.dylib libba.dylib
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -c -o main.o main.cc
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o bar.dylib bar.cc
clang++-mp-3.4 -std=c++11 -fvisibility=hidden -shared -o baz.dylib baz.cc
.clang++-mp-3.4 -o main main.o bar.dylib baz.dylib
$ ./main
bar value: 0x1012ae0f0
bar type:  0x1012b3090
bar equal: 0
std::bad_cast

Есть также три экземпляра типа (удаление неисправного dynamic_cast действительно показывает, что их три), но на этот раз они не равны, и dynamic_cast (конечно) не удается.

Теперь вопрос превращается в:
1. это различие между обоими компиляторами, желаемое их авторами
2. если нет, то каково «ожидаемое» поведение между обоими

Я предпочитаю семантику GCC, так как она позволяет реально реализовать стирание типов без необходимости публичного раскрытия упакованных типов.

13

Решение

Я сообщил об этом людям из LLVM, и это было впервые отметил что если это работает в случае GCC, это потому что:

Я думаю, что разница на самом деле в библиотеке C ++. Это выглядит как
libstdc ++ изменен, чтобы всегда использовать strcmp имен typeinfo:

https://gcc.gnu.org/viewcvs/gcc?view=revision&Пересмотр = 149964

Должны ли мы сделать то же самое с libc ++?

На это было четко ответил, что:

Нет. Это пессимизирует правильно ведущий себя код, чтобы обойти код, который
нарушает ЭЛЬФ ABI. Рассмотрим приложение, которое загружает плагины с
RTLD_LOCAL. Два плагина реализуют (скрытый) тип под названием «Плагин».
Изменение GCC теперь делает эти совершенно разные типы идентичными для всех
Цели RTTI. Это не имеет никакого смысла.

Поэтому я не могу делать то, что я хочу с Clang: уменьшить количество опубликованных символов. Но, похоже, это разумнее, чем нынешнее поведение GCC. Очень плохо.

6

Другие решения

Недавно я столкнулся с этой проблемой, и @akim (ОП) диагностировал ее.

Обходной путь должен написать свой собственный dynamic_cast_to_private_exact_type<T> или что-то такое, что проверяет typeidСтроковое имя

template<class T>
struct dynamic_cast_to_exact_type_helper;
template<class T>
struct dynamic_cast_to_exact_type_helper<T*>
{
template<class U>
T* operator()(U* u) const {
if (!u) return nullptr;
auto const& uid = typeid(*u);
auto const& tid = typeid(T);
if (uid == tid) return static_cast<T*>(u); // shortcut
if (uid.hash_code() != tid.hash_code()) return nullptr; // hash compare to reject faster
if (uid.name() == tid.name()) return static_cast<T*>(u); // compare names
return nullptr;
}
};
template<class T>
struct dynamic_cast_to_exact_type_helper<T&>
{
template<class U>
T& operator()(U& u) const {
T* r = dynamic_cast_to_exact_type<T&>{}(std::addressof(u));
if (!r) throw std::bad_cast{};
return *r;
}
}
template<class T, class U>
T dynamic_cast_to_exact_type( U&& u ) {
return dynamic_cast_to_exact_type_helper<T>{}( std::forward<U>(u) );
}

Обратите внимание, что это может иметь ложные срабатывания, если два модуля имеют разные Foo тип, который не связан. Модули должны помещать свои закрытые типы в анонимные пространства имен, чтобы избежать этого.

Я не знаю, как аналогично обрабатывать промежуточные типы, так как мы можем проверить только точный тип в typeid сравнение и не может перебрать дерево наследования типов.

0

По вопросам рекламы [email protected]