【发布时间】:2021-11-04 08:54:34
【问题描述】:
我将编写一个共享库,我在 Internet 上找到了有关设置符号可见性的 note。一般指导是隐藏库的客户端不需要的所有内容,从而减少库的大小和加载时间。除非涉及到使用类层次结构,否则对我来说很清楚。
但是,让我们从头开始。
假设:
- GCC 工具链用于 Linux 操作系统
- 库和客户端应用程序使用相同的工具链构建
- 该库将用于使用相同工具链构建的 Linux 发行版
-
-fvisibility=hidden和-fvisibility-inlines-hidden编译选项用于构建库
我准备了一些案例来检查编译器切换到库的影响。
案例一
客户端代码:
#include "Foo.hpp"
int main()
{
auto s = make();
delete s;
}
库头:
struct Foo{};
Foo* make() __attribute__((visibility("default")));
库来源:
#include "Foo.hpp"
Foo *make() { return new Foo; }
在这种情况下,一切编译和链接都没有错误,甚至只导出了make 函数。这一点我很清楚。
案例2
我添加了Foo 析构函数定义。
客户端代码与案例 2 相同。
库头:
struct Foo {
~Foo();
};
Foo* make() __attribute__((visibility("default")));
库来源:
#include "Foo.hpp"
Foo::~Foo() = default;
Foo *make() { return new Foo; }
在这种情况下,在将客户端应用程序与库链接期间,链接器抱怨对 Foo::~Foo() 的未定义引用,这对我来说也很清楚。析构函数符号未导出,客户端应用程序需要它。
案例3
客户端应用程序和库源与案例 2 相同。但是,在库头中我导出 Foo 类:
struct __attribute__((visibility("default"))) Foo {
~Foo();
};
Foo* make() __attribute__((visibility("default")));
这里没有惊喜。由于库导出了客户端应用程序所需的所有符号,因此代码编译、链接和运行不会出错。
但是(案例 4)
当我第一次尝试编写这个库时,我没有导出类,而是将析构函数设为虚拟:
struct Foo {
virtual ~Foo();
};
Foo* make() __attribute__((visibility("default")));
令人惊讶的是……编译、链接和运行的代码没有任何错误。
更进一步(案例 5)
最初我的库定义了一个类层次结构,其中Foo 是其余部分的基类,并定义了纯虚拟接口:
struct __attribute__((visibility("default"))) Foo {
virtual ~Foo();
virtual void foo() = 0;
};
Foo* make() __attribute__((visibility("default")));
make 函数产生派生类的实例,返回一个指向基类的指针:
#include "Foo.hpp"
#include <cstdio>
Foo::~Foo() = default;
struct Bar: public Foo
{
void foo() override { puts("Bar::foo"); }
};
Foo* make() { return new Bar; }
应用程序使用接口:
#include "Foo.hpp"
int main()
{
auto s = make();
s->foo();
delete s;
}
一切编译、链接、运行都没有错误。应用程序打印Bar::foo,这表明调用了Bar::foo覆盖,甚至根本没有导出Bar类。
问题
- (案例 4)为什么链接器没有向析构函数抱怨缺少符号?这是一种未定义的行为吗?我不会那样做,只是想了解。
- (案例 5)是否可以只为基类和工厂函数导出符号,其中工厂返回隐藏符号的派生类的实例?
【问题讨论】:
-
关于案例 4,我认为这是可行的,因为析构函数是使用虚拟方法表间接调用的。链接器不需要在链接时解析实际功能;这是在运行时完成的。
标签: c++ linker shared-libraries linker-flags