Skip to content
Advertisement

Linux C++ Dynamic Libs and static initialization order

Forgive me on the long post. Its a complicate problem I wanted a complete description.

On Linux Fedora 21 (g++ 4.9.2) and like.

I’m working on a C++ database wrapper library using a base class of “database” and inherited classes of Oracle and Sybase.

Beyond that, I wanted some of the programs to be able to dynamically load the libraries at runtime with dlopen, following the template from http://www.linuxjournal.com/article/3687?page=0,0 and others.

Other programs however to directly link the version they need. The problem with my solution is there exists a static std::map in database.cpp (see code below) that must be initialized before it is assigned in the static initializer of oracle / sybase.cpp. The only way I’ve been able to accomplish this is by the physical order of the -l arguments at compile time (see Makefile). Get them correct:, program runs fine. Get them backwards: — and program compiles & links find, but will crash immediately on execution. This troubles me as prefer a successful compile/link to yield a successful run and a seemly flip of library order usually doesn’t do this. I know not uncommon for link order to cause link errors, but not runtime errors.

Two questions:

  1. Is there some other link option I can use to ensure proper initialization besides library order?
  2. Can anyone see another algorithm that would eliminate this dependancy. (I don’t want the normal programs to have to declare DBFactory, it needs to stay in the libraries, but an extra step in main_dlopen would be fine).

Here is the code, all example modules and Makefile. 2 main programs are included, one for normal linking (main_direct) and one for runtime linking (main_dlopen). Both will compile and run as given (unless there is a cut-n-paste typo).

Thanks for your attention…

//  database.h
#include <map>
#include <string>
#include <iostream>
#include <stdio.h>

class Database; // forward declaration
typedef Database* maker_t();
// our global factory
typedef std::map<std::string, maker_t *> DBFactory_t;
extern DBFactory_t DBFactory;

class Database 
{
    public:
        Database () {}
        virtual ~Database () {};  
};

// database.cpp
#include "database.h"

__attribute__((constructor)) void fooDatabase (void) {
    fprintf (stderr, "Database library loadedn");
}

// our global factory for making dynamic loading possible
// this is the problem child static global
std::map<std::string, maker_t * > DBFactory ;

// oracle.h
#include "database.h"
class Oracle : public Database
{
    public:
     Oracle ();
     virtual ~Oracle ();
};

//  oracle.cpp class.
#include "oracle.h"
using namespace std;
__attribute__((constructor)) void fooOracle (void) {
    fprintf (stderr, "Oracle library loadedn");
}
// the following code follows the referece at 
// http://www.linuxjournal.com/article/3687?page=0,0
extern "C" {
    Database * Maker()
    {
        return new Oracle;
    }
    class proxy  {
        public:
        proxy () 
        {
            // register the maker with the factory
            fprintf (stderr, "Oracle Proxy Constructorn");
            DBFactory["ORACLE"] = Maker;
        }
    };
}
proxy p;
Oracle::Oracle () {
    cout << "Oracle Constructor" << endl;
}
Oracle::~Oracle ()
{
    cout << "Oracle Destructor" << endl;
}

// sybase.h
#include "database.h"
class Sybase : public Database
{
    public:
     Sybase ();
     virtual ~Sybase();
};

// sybase.cpp class.
#include "sybase.h"
using namespace std;
__attribute__((constructor)) void fooSybase (void) {
    fprintf (stderr, "Sybase library loadedn");
}
extern "C" {
    Database * Maker()
    {
        return new Sybase;
    }
    class proxy  {
        public:
        proxy () 
        {
            // register the maker with the factory
            fprintf (stderr, "Sybase Proxy Constructorn");
            DBFactory["SYBASE"] = Maker;
        }
    };
}
proxy p;
Sybase::Sybase () {
    cout << "Sybase Constructor" << endl;
}
Sybase::~Sybase ()
{
    cout << "Sybase Destructor" << endl;
}

// main_direct.cpp
#include "oracle.h"

int main ()
{
    Oracle db ();
    return 0;
}

// main_dlopen.cpp
#include <iostream>
#include <dlfcn.h>
#include <stdlib.h>
#include "database.h"
using namespace std;

int main ()
{
    void * dl = dlopen ("libSybase.so", RTLD_NOW);
    if (!dl) { cerr << dlerror() << endl; exit (1); }

    Database * db = DBFactory["SYBASE"] ();

    delete db;
    return 0;
}

#Makefile
CXXFLAGS = -Wall -fPIC -ggdb -std=c++11
all:  main libOracle.so libSybase.so libdb.so

main:   main_dlopen main_direct

main_dlopen: main_dlopen.o libdb.so libOracle.so libSybase.so
    ${CXX} -o main_dlopen main_dlopen.o -L. -ldb -ldl

#  reverse -lOracle and -ldb to make this program crash
main_direct: main_direct.o libdb.so libOracle.so libSybase.so
    ${CXX} -o main_direct main_direct.o -L. -lOracle -ldb

libOracle.so:  oracle.o
    ${CXX} -shared -fPIC -o libOracle.so oracle.o

libSybase.so:  sybase.o
    ${CXX} -shared -fPIC -o libSybase.so sybase.o

libdb.so:  database.o
    ${CXX} -shared -fPIC -o libdb.so database.o

clean:
    rm -f *.o *.so main_dlopen main_direct

%.o : %.cpp 
    ${CXX} ${CXXFLAGS} -c $< -o $@

Advertisement

Answer

This is a fairly standard problem: you have global data, with no way to control when it is initialized.

There is also a standard solution to that problem: indirect this data intialization through a function call.

Instead of having global std::map<...> DBFactory, do this:

// database.cpp
DBFactory_t& getDBFactory() {
  static DBFactory_t factory;
  return factory;
}

// oracle.cpp
    proxy () 
    {
        // register the maker with the factory
        fprintf (stderr, "Oracle Proxy Constructorn");
        getDBFactory()["ORACLE"] = Maker;
    }

Voila: the factory will be constructed the first time you need it.

You have an additional problem, which you haven’t identified yet: both oracle.cpp and sybase.cpp define function Maker, class proxy and variable p in global namespace, leading to ODR violation and undefined behavior if both files are loaded into a single process. You better use separate namespaces to avoid that.

Advertisement