使用clang++,-fvisibility=hidden和typeinfo以及类型抹除

24
这是我在Mac OS X上使用clang++时遇到的问题的简化版本。为了使问题更真实,对其进行了严格编辑(第一次尝试描述该问题时并未展现问题)。
失败原因:
我有一个大型C++软件,其中目标文件中有大量符号,因此我使用“-fvisibility=hidden”来保持我的符号表小。众所周知,在这种情况下,人们必须特别注意虚函数表,而我想我面临了这个问题。但是,我不知道如何以一种既能让gcc又能让clang满意的方式优雅地解决它。
考虑一个具有向下转换运算符“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);

我有两个不同的编译单元提供了类似的服务,可以近似地看作是相同特征的两倍:从basederive<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

在OS X上,使用gcc(4.8)可以成功运行:

$ 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.ccbaz.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的语义,因为它允许真正实现类型抹除,而不需要公开包装类型。

static_cast 能够正常工作,在我的情况下,我并不真正需要 dynamic_cast,因为只有有效的参数被传递给了 as。然而,我喜欢编译器/运行时进行双重检查,使用 static_cast 对我来说就像是产品准备好了,而 dynamic_cast 则用于调试。所以我真的想使用 dynamic_cast - akim
就这个示例而言,我只需将“API”添加到“derived”中即可使其正常工作。但是,在我的实际问题中,这种方法并不起作用,我还不知道完全解决问题和这个小抽象案例之间的区别在哪里。 - akim
我已经编辑了最初的问题,以更好地反映问题,因此我的先前评论(使derived公开)不再适用。 - akim
我认为这与模板如何和在哪里实例化有关。dynamic_cast利用来自有效载荷的RTTI,该RTTI可能在需要它的编译单元中不可用(由于某种原因)。GCC和Clang可能有不同的解决方法。 - doron
  1. 文件扩展名告诉编译器所包含的代码类型。
  2. 尽管XCode声称支持c11标准,但事实并非如此。我个人已经提交了几个错误报告,但苹果表示他们没有意图修复。希望这可以帮到你。
- Dan
显示剩余2条评论
2个回答

11

我已向LLVM的人员报告了这个问题,并且 最初指出,如果在GCC的情况下它起作用,是因为:

我认为区别实际上在于C ++库。看起来libstdc ++更改为始终使用类型信息名称的strcmp:

https://gcc.gnu.org/viewcvs/gcc?view=revision&revision=149964

我们应该对libc ++做同样的事情吗?

对此, 明确回答:

不应该。它会使正确行为的代码变悲观,以解决违反ELF ABI的代码。考虑一个使用RTLD_LOCAL加载插件的应用程序。两个插件实现了名为“Plugin”的(隐藏)类型。GCC的更改现在使这些完全独立的类型在所有RTTI目的上都相同。这根本没有意义。

所以我不能用Clang做我想要的事情:减少发布的符号数量。但它似乎比GCC的当前行为更合理。太糟糕了。


1
我最近遇到了这个问题,@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比较中检查确切类型,而不能迭代类型继承树。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接