I will be writing a shared library and I've found this note on the Internet about setting the visibility of the symbols. The general guidance is to hide everything that is not needed by the client of the library, which leads to reduce the size and the load time of the library. And it's clear for me unless it comes to use class hierarchies.
However, let's start from the beginning.
-fvisibility=hidden
and -fvisibility-inlines-hidden
compile option are used to build libraryI've prepared cases to check what is the impact of the compiler switches to the library.
Client code:
#include "Foo.hpp"
int main()
{
auto s = make();
delete s;
}
Library header:
struct Foo{};
Foo* make() __attribute__((visibility("default")));
Library source:
#include "Foo.hpp"
Foo *make() { return new Foo; }
In this case, everything compiles and links without errors, even only the make
function is exported. This is clear to me.
I add the Foo
destructor definition.
Client code same as in Case 2.
Library header:
struct Foo {
~Foo();
};
Foo* make() __attribute__((visibility("default")));
Library source:
#include "Foo.hpp"
Foo::~Foo() = default;
Foo *make() { return new Foo; }
In this case, during linking the client application with the library, the linker complains about the undefined reference to Foo::~Foo()
, which is also clear for me. The destructor symbol was not exported and the client application needs it.
The client application and library source are the same as in case 2. However, in the library header I export Foo class:
struct __attribute__((visibility("default"))) Foo {
~Foo();
};
Foo* make() __attribute__((visibility("default")));
No surprises here. Code compiles, links and runs without errors since the library exports all the symbols needed by the client application.
When I was writing this library on the first attempt, instead of export the class I've made the destructor virtual:
struct Foo {
virtual ~Foo();
};
Foo* make() __attribute__((visibility("default")));
And surprisingly… the code compiled, linked and runs without any errors.
Originally my library defines a class hierarchy, where Foo
is the base class for the rest and defines pure virtual interface:
struct __attribute__((visibility("default"))) Foo {
virtual ~Foo();
virtual void foo() = 0;
};
Foo* make() __attribute__((visibility("default")));
The make
function produces instances of the derived classes, returning a pointer to the base class:
#include "Foo.hpp"
#include <cstdio>
Foo::~Foo() = default;
struct Bar: public Foo
{
void foo() override { puts("Bar::foo"); }
};
Foo* make() { return new Bar; }
And application uses the interface:
#include "Foo.hpp"
int main()
{
auto s = make();
s->foo();
delete s;
}
Everything compiles, links, runs without errors. Application prints Bar::foo
, which shows that the Bar::foo
override was called, even the Bar
class was not exported at all.
Why did the linker not complain about the missing symbol to the destructor?
That's because client code loads address of destructor from vtable stored in object created in make
function. Linker does not need to know the explicit address of ~Foo
when linking the client code.
Is it ok to export symbols only for the base class and the factory function, where the factory returns instances of the derived classes for which the symbols are hidden?
It is ok as long as your client code only calls overloaded virtual methods in these derived classes. The reasons are same as for virtual ~Foo
- addresses will be obtained from the object's vtable.