Using Factory Functions to Export C++ Objects from Shared Libraries / DLLs, but also applying Factory Class Design Pattern.

(Edited 3/15/2021, 22h45: )

The main reason for which factory functions are sometimes used is the fact that, while object-oriented code can in fact be compiled into shared libraries, situations exist in which the application programmer is not aware of specific libraries that will need to be loaded at run-time. As a result, this developer cannot specify them during the linking stage of his or her application.

(End of Edit, 3/15/2021, 22h45.)

What one does in practical cases is, to define “factory functions” in the shared library, such as the function “maker()” in the example below, and to give the compiler directive ‘extern “C”‘ when doing so:

https://www.linuxjournal.com/article/3687

I think that the example I’ve just linked to suffers from some seriously bad typesetting. But to my mind, it gets the point across. The methods that belong to C++ classes have mangled names, which are difficult to load from shared libraries directly (using the ‘dlsym()’ function). In principle, one could try to predict the name mangling in the client program, but in practice, this is avoided. Instead, a factory function is exported from the shared library to the client program, the name of which is explicitly not mangled, due to this compiler directive, and what that function does when called is, to create an object of the specified type. The client program’s C++ ‘knows’ what methods of the object it can call because of header files…

When I was studying System Software at Concordia University, an exercise the whole class was required to carry out was, to define a function that was declared with ‘extern “C”‘, to store that in a shared library, and, to write a client program which loaded that function. We were never required to load C++ objects from that shared library. But, the article which I just linked to above, explains how to do that, at least well enough so that I can follow it.

(…)

If you’re one of those people like me, who have seen C++ with many ‘Create…()’ function-names, even though we know that in general, in C++, the constructors of class-objects have the same name as the class, what is written above is likely the reason, for which so many Creator functions have been defined.

I think, though, that there is one way in which I must second-guess the author of the article I just linked to. He ended up using the ‘extern “C”‘ directive in more places than he needed to. If it was something his project set out to do from the beginning, for the source, shared libraries to register their factory functions in an array of function-pointers automatically, then there is really no longer any reason, why their names should not be mangled. In fact, in certain cases conflicts could result, as soon as two functions have the same name, such as “proxy()”. And so, in such cases, there is really only one object in the whole process, the name of which must not be mangled, and in the case cited above, that would be the associative array.

Another fact which the cited article does not mention is simply, that it might result in tedious code, if the factory function was defined separately, for every class of objects that a shared library can export. It might simplify things, if a template Creator class could be defined, which has a factory method as one of its methods, which can be applied to a series of classes that have default constructors… The following article describes a slightly different use:

https://refactoring.guru/design-patterns/factory-method/cpp/example

Of course, the cited function ‘SomeOperation()’ could not be used…

//  The following lines should probably go into some header file.

#include <map>
#include <string>
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>

using std::string;

//  If the library existed, either IMPORT or WINDLL would be defined...

typedef std::map<string, void *> function_array;

function_array *reg_factories_p = NULL;

#ifdef WIN32
#define DLL_EXPORT extern "C" __declspec(dllexport)
#define DLL_IMPORT extern "C" __declspec(dllimport)
#else
#define DLL_EXPORT extern "C"
#define DLL_IMPORT extern "C"
#endif

#ifdef WINDLL
DLL_EXPORT function_array reg_factories;
function_array reg_factories;
#else
#ifndef IMPORT
function_array reg_factories;
#endif
#endif

//  What comes below this line, goes into the shared library / DLL.

class Obj_1 {
public:
Obj_1() {}
};

class Obj_2 {
public:
Obj_2() {}
};

class Obj_3 {
public:
Obj_3() {}
};

//  Template definition (can't be compiled).

template<class T>
class Creator {
public:

typedef T * (Creator<T>::*method)() const;
method m_ptr;

Creator(string object_name) {
m_ptr = &Creator<T>::FactoryMethod;
#ifndef IMPORT
if (! reg_factories_p) {
reg_factories_p = &reg_factories;
}
reg_factories[object_name] = (void *) this->m_ptr;
#endif
}

T *FactoryMethod() const {
return new T;
}

};

//  Template instantiations (can be compiled).

Creator<Obj_1>  mk1("Object1");
Creator<Obj_2>  mk2("Object2");
Creator<Obj_3>  mk3("Object3");

//  Static objects will be constructed, if the shared library
//  has been opened with 'RTLD_NOW'.
//  This will cause the constructor within 'Creator' to be
//  called, and therefore, the associative array to be populated.

//  Code below this line is meant to go into the client program...

//  The following template class will serve to cast void pointers back
//  to Maker objects, each with a method that makes the object...

#ifndef WINDLL

#define LIB_NAME "./libfunnylib.so"

template<class T>
class Maker {

public:
typedef T * (*obj_maker) ();

obj_maker m_fptr;

Maker(void *fptr) {
m_fptr = reinterpret_cast<obj_maker> (fptr);
}

T * Make() {
return (*m_fptr) ();
}

};

int main(int argc, char* argv[]) {

void *hndl = dlopen(LIB_NAME, RTLD_NOW);
if(hndl == NULL) {
printf("%s\n\n", dlerror());
} else {
void *symbol = dlsym(hndl, "reg_factories");
if (symbol) {
reg_factories_p = (function_array *) symbol;
} else {
printf("%s Did not export symbol reg_factories.\n\n", LIB_NAME);
return -1;
}
}

char *buffer;
size_t bufsize = 32;

printf("The purpose here is, to find out whether\n");
printf("I was able to store non-trivial adresses in the\n");
printf("associative array.\n");
printf("\n");
printf("Object1 factory address: %p\n", (*reg_factories_p)["Object1"]);
printf("Object2 factory address: %p\n", (*reg_factories_p)["Object2"]);
printf("Object3 factory address: %p\n", (*reg_factories_p)["Object3"]);
printf("\n");
printf("Now attempting to execute those factories...\n");

//  Exploiting the compiler's willingness to create temporary
//  objects, invoked directly by their class-name...
try {
Maker<Obj_1>((*reg_factories_p)["Object1"]).Make();
Maker<Obj_2>((*reg_factories_p)["Object2"]).Make();
Maker<Obj_3>((*reg_factories_p)["Object3"]).Make();
} catch (...) {
printf("Some type of error took place!\n");
return -1;
}

printf("All pointer casts executed.\n\n");

printf("Press Enter to quit program.\n");

buffer = (char *) malloc(bufsize * sizeof(char));
if( buffer == NULL)
{
perror("Unable to allocate buffer");
exit(-1);
}

getline(&buffer,&bufsize,stdin);

//  We're done. Cleaning up.

free(buffer);

return 0;
}
#endif



One fact which must be kept in mind, if this code is ever to be used ‘in the real world’, is, that The associative array should be declared as ‘extern “C”‘ in a header file, but additionally allocated wherever it’s going to be used.  This results in two similar-looking C++ statements, when building the shared library.

Also, compiling this code will generate one warning per factory method, unless, under ‘g++’, the flag ‘-Wno-pmf-conversions‘ is set. Additionally, on Linux computers, linking requires that the flag ‘-ldl‘ be set.

Now, an astute reader will ask, ‘Mainstream code can be compiled without requiring special flags. Why does this guy’s code require that special flags be set?‘ And the answer to that question is, ‘Because this code cheats. The warning which up-to-date compilers will generate – at best – exists, because on some platforms, a pointer-to-method cannot be cast to a pointer-to free function, in the form of a single address, and always work. Therefore, this practice is actually shunned.’

At the end of this posting, I will show compatible code, that does not cheat…

(Updated 3/22/2021, 3h55… )