Exporting Symbols

The Arbor libraries are compiled with hidden visibility by default which strips a compiled library of all symbols not explicitly marked as visible. Note that the term visibility here refers to the symbol visibility exposed to the linker and not to language access specifier such as public/private. Arbor provides a couple of macros to make functions and classes visible which are defined in the header file export.hpp in each library’s include directory, i.e. include/arbor/export.hpp. These header files are generated at configure time based on the build variant, compiler and platform.

By default, hidden/visible symbols will affect shared libraries directly. Since the linker is responsible for making symbols hidden, the visibility settings are not applied to static libraries immediately, as static libraries are generated by bundling object files with the archiver (standard in cmake for STATIC libraries). In principle, the linker would be able to generate relocatable output instead (ld -r) by incrementally linking object files into one output file (sometimes called partial linking) which would apply proper visibility to static libraries. However, we currently do not handle this case in our build scripts as it is not not nativley supported by cmake yet.

Note

When linking an application with static Arbor libraries the linker may issue warnings (particularly on macos). Thus, if you encounter problems, try building shared Arbor libraries (cmake option -DBUILD_SHARED_LIBS=ON) instead.

Macro Description

ARB_LIBNAME_API

Here “LIBNAME” is a placeholder for the library’s name: ARB_ARBOR_API for the core Arbor library, ARB_ARBORIO_API for Arborio, etc. This macro is intended to annotate functions, classes and structs which need to be accessible when interfacing with the library. Note that it expands to different values when Arbor is being built vs. when Arbor is being used by an application.

Below we list the places where the macro needs to be added or can be safely omitted (we assume all of the symbols below are part of the public/user-facing API). In general, annotation is required

  • for declarations in header files which are not definitions

  • for definitions of functions, friend functions and (extern) variables in source files

Members and member functions of already visible classes will be visible, as well, without further annotation, except for friend function (see below).

Implementation details and internal APIs may not need annotation as long as they do not require visibility across the library boundary (though some annotations are required for unit test purposes). Exception classes and type-erased objects need special annotation, see ARB_SYMBOL_VISIBLE.

header.hpp
#include <arbor/export.hpp>

// free function declaration
ARB_ARBOR_API void foo();

// free function definition
inline void bar(int i) { /* ... */ }

// function template definition
template<typename T>
void baz(T i) { /* ... */ }

// class definition
// class member declaration
class ARB_ARBOR_API A {
    A();
    friend std::ostream& operator<<(std::ostream& o, A const & a);
};

// class definition
// class member definition
class B {
    B() { /* ... */ }
};

// template class definition
// class member definition
template<typename T>
class C {
    C() { /* ... */ }
};

// (extern) variable declarations
ARB_ARBOR_API int g;
ARB_ARBOR_API extern int h;
source.cpp
// free function definition
ARB_ARBOR_API void foo() { /* ... */ }

// class member definition (will be visible since A is visible)
A::A() { /* ... */ }

// friend function definition
ARB_ARBOR_API std::ostream& operator<<(std::ostream& o, A const& a) { /* ... */ }

// (extern) variable definitions
ARB_ARBOR_API int g = 10;
ARB_ARBOR_API int h = 11;
ARB_SYMBOL_VISIBLE

Objects which are type-erased and passed across the library boundaries sometimes need runtime type information (rtti). In particular, exception classes and classes stored in std::any or similar need to have the correct runtime information attached. Hidden visibility strips away this information which leads to all kind of unexpected behaviour. Therefore, all such classes must be annotated with this macro which guarantees that the symbol is always visible. This also applies for classes (or structs) which are entirely defined inline. Note, one must not use ARB_LIBNAME_API for these cases.

header.hpp
#include <arbor/export.hpp>

// exception class defintion and class member definition
class ARB_SYMBOL_VISIBLE some_error : public std::runtime_error {
    some_error() { /* ... */ }
};

// class defintion and member defintion
// class D will be type-erased and restored by an any_cast or similar
class ARB_SYMBOL_VISIBLE D {
    D() { /* ... */ }
};