Search code examples
c++linkershared-librarieslinker-flags

Hiding symbols of the derived class in shared library


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.

Assumptions:

  • The GCC toolchain is used on Linux OS
  • The library and the client application are built with the same toolchain
  • The library will be used on Linux distribution built with the same toolchain
  • -fvisibility=hidden and -fvisibility-inlines-hidden compile option are used to build library

I've prepared cases to check what is the impact of the compiler switches to the library.

Case 1

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.

Case 2

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.

Case 3

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.

However (case 4)

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.

Go further (case 5)

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.

Questions

  1. (Case 4) Why did the linker not complain about the missing symbol to the destructor? Is it kind of the undefined behaviour? I'm not going to do it like that, only want to understand.
  2. (Case 5) 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?

Solution

  • 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.