Library API Design

What is an API

Application Program Interface

The set of functions and types that a library provides

Contents

  • Kinds of libraries
    • Header only
    • Static
    • Dynamic/shared
  • Symbol linkage
  • interfaces
  • Heap allocated objects
  • std::cross library

Header only libraries

A collection of header files

Pros

  • Very easy to use - just copy the files and #include
  • No overhead at all - even some calls might get inlined

Cons

  • No isolation at all - the client sees all implementation details
  • Changes in the library require recompiles
  • Difficult to use with non-open-source licenses
  • May significantly increase compilation time
  • Tends to creep all over the place

Static Libraries

An archive of object files

Pros

  • Relatively easy to use - include and link
  • No overhead, some calls might be inlined if we are using optimizing linker

Cons

  • The compiler options must match perfectly between the library and the clients
  • Changing the library requires recompilation of the client code
  • 3rd party dependencies conflict

Dynamic/Shared Libraries

a single object file, that allows calling of predefined set of functions

Pros

  • Almost complete isolation between library and client
  • Changing the library without recompilation can be achieved

Cons

  • Non standard
  • A lot of platform specifics
  • DLL boundary

Symbol Visibility

  • Windows - hidden by default
  • Others - visible by default

Symbol Visibility

  • Windows
    __declspec(dllimport)
    __declspec(dllexport)
    
  • g++ / clang++
    // compile with flags
    -fvisibility=hidden -fvisibility-inlines-hidden
    __attribute__ ((visibility ("default")))
    

dllimport \ dllexport

__declspec(dllexport) int answer = 42;

__declspec(dllexport) int get_answer() {
  return 42;
}

dllimport \ dllexport

class __declspec(dllexport) Renderer {
  public:
    void render(const Texture& texture);
};

class Texture {
  public:
    __declspec(dllexport) void fill(Color color);
    
    void fill(byte r, byte g, byte b); // not exported
};

dllexport

Makes symbols to be exported from the DLL - visible to clients

dllimport

Makes symbols to be imported from a DLL, the linker is going to look for these symbols in all linked DLLs

dllimport \ dllexport

So the library needs to say dllexport and the client needs to see dllimport for the same symbol

MY_LIBRARY_API

#if defined(MY_LIBRARY_IMPL)
  #define MY_LIBRARY_API __declspec(dllexport)
#else
  #define MY_LIBRARY_API __declspec(dllimport)
#endif

MY_LIBRARY_API int answer();

Compile the library with MY_LIBRARY_IMPL defined

MY_LIBRARY_API

#if defined(_WIN32)
  #define MY_LIBRARY_EXPORT __declspec(dllexport)
  #define MY_LIBRARY_IMPORT __declspec(dllimport)
#else
  #define MY_LIBRARY_EXPORT __attribute__ ((visibility ("default")))
  #define MY_LIBRARY_IMPORT
#endif

#if defined(MY_LIBRARY_IMPL)
  #define MY_LIBRARY_API MY_LIBRARY_EXPORT
#else
  #define MY_LIBRARY_API MY_LIBRARY_IMPORT
#endif

Non-Windows & 3rd party dependencies

Controlling visibility of symbols - lack of internal linkage

Symbol Maps

VERSION
{
        global:
                extern "C++" {
                        library*;
                };
        local: *;
};

Link with:

-Wl,--version-script=library.map

Lifetime of an application

  1. preprocess
  2. compile time
  3. link time
  1. load time
  2. run time

Linux

Symbol resolution is done at load time, not at link time

  • undefined symbols in the library are reported when linking an executable, not the library itself
  • Symbols in the executable of a 3rd party library can override your own symbols
# link with
g++ *.o -Xlinker -Bsymbolic -o myLibrary.so

API

  1. Pure C
  2. C++ with std::

Pure C API

Pros

  • the most portable - C has no mangling
  • easy to use with other languages
    • luajit's libffi (Foreign Function interface)
    • Python - ctype, cffi
    • C# (.NET) - PInvoke

Cons

  • no C++ features - resource management, OOP, generics
  • prefix on every function

Opaque Structure

// just a forward declaration
typedef struct lua_State lua_State;
lua_State* lua_newstate();
void lua_call(lua_State* state, int arguments, int results);
void lua_close(lua_State*);

C++ with C API

typedef struct lua_State lua_State;
extern "C" lua_State* lua_newstate();
extern "C" {
  int lua_compile(lua_State* state, const char* file);
  int lua_run(lua_State* state, int);
}
struct lua_State {
  bool compile(const char* file);
} ;

lua_State* lua_newstate() {
  return new lua_State;
}

int lua_compile(lua_State* state, const char* file) {
  return state->compile(file)? 1 : 0;
}

Having std:: in the API

  • Works only for header-only libraries or libraries that are compiled by the user
  • Or there must be a separate compiliant for each possible compiler / runtime / flags combination

std::

The standard doesn't define implementation details for the standard library. So implementations may differ. For example:

// GCC
class pseudo_vector {
  T* begin;
  T* end;
  T* end_of_storage;
};

// MSVS
class pseudo_vector {
  T* begin;
  T* end_of_storage;
  T* end;
};
namespace library {
  void add_random(std::vector<int>& numbers) {
    numbers.push_back(rand());
  }
}

Implementation may be different when using different compiler options

Visual Studio has debug iterators enabled in Debug build. They allow detection of incorrect usage of STL. For example, using an iterator to a vector, after it has been invalidated.

In Release a vector::iterator can be a plain pointer, but a debug iterator is actually a smart pointer

Memory allocation

Allocated memory must be freed from the allocator that allocated it.

  • the library and the client MUST use the same allocator
  • the deallocation MUST happen in the same module as the corresponding allocation

STL hides allocations from the user, so it is impossible to be sure that the memory will be deallocated from the allocator that allocated it.

C with classes / COM

  • a extern "C" function that initializes the library and returns an object that implements the library interface
  • functions and methods take only primitive types and pointers to interfaces

Lua C with classes style - declaration

namespace Lua {
  class State {
    virtual void ~State() = 0;
    virtual void destroy() = 0;
    virtual bool compile(const char* file) = 0;
  };
}
extern Lua::State* lua_new_state();

Lua C with classes style - implementation

namespace Lua {

  class StateImpl: public State {
  
    virtual void ~StateImpl() {
    }
    
    virtual void destroy() override {
      delete this;
    }
    
    virtual bool compile(const char* file) override {
      /...
    }
  };
}

Lua C with classes style - implementation

Lua::State* lua_newstate() {
  return new StateImpl;
}
namespace Lua {
  void StateImpl::destroy() override {
    delete this;
  }
}

Enforces that the StateImpl object is deallocated using the allocator that allocated it

Lua C with classes style - client

auto lua = lua_new_state();
lua->compile("hello.lua");
lua->destroy();

Lua C with classes style - resources

struct LuaDestroyer {
  void operator(Lua::State* lua) {
    lua->destroy();
  }
};

std::unique_ptr<Lua::State, LuaDestroyer> lua(lua_new_state());
lua->compile("hello.lua");

Why not exporting the StateImpl class directly?

  • Have to export all of its base classes and member types
  • Exposes the implementation to the client. Client needs to be recompiled if the implementation changes
  • Will work only with one compiler because of the different name mangling schemes

Why exporting an abstract class works

An abstract class is just a virtual table - it matches the COM model, and all compilers implement it in the same way.

Resources

[HowTo: Export C++ classes from a DLL] (http://www.codeproject.com/Articles/28969/HowTo-Export-C-classes-from-a-DLL)

pImpl

Pointer-to-Implementation is a technique for reducing coupling between a class (a library) and its clients

Pros

  • Allows the class implementation to change with recompiling the client
  • Reducing compilation times by including expensive headers only in a single source file

Cons

  • Prevents function inlining
  • Adds an extra redirection for each call

Resources

Usage of pImpl

Almost every project is divided in sub-projects according to some principles. For example:

  • Implementation: separating platform specific code
  • Aspect: logging, profiling
  • Functionality: Rendering

Usage of pImpl

pImpl can be used to implement the interfaces of the internal libraries in a project