Write an Interface between your Program and a Shared Object

I wanted to share something I’ve been doing for some hours today, in the hope to understand it better. I also hope to receive criticism on this, because there might be a better way to do the same thing, only more elegantly. This is no research problem, as I am sure it has been solved time and again in different contexts; but the whole example, comprising all the pieces put together in this post, is something I have not seen around, and I think it can be of some service if I explain it.

Problem

We have an application that uses a resource offered by the operating system. I am on a GNU/Linux, so for example the resource is a file in the proc/ file system, say version. The application works like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "server_operations.hpp"
#include <iostream>

using std::cout;

int main() {
ServiceOps ops;
string result = ops.GetResource("version");
cout << "Result is:\n\"\"\"";
cout << result;
cout << "\"\"\"";
return 0;
}

The class ServiceOps encapsulates the fact that the resource with the name version is actually a file being opened. It has the following structure:

1
2
3
4
class ServiceOps {
public:
std::string GetResource(const std::string& name);
};

The implementation just opens a file with name name, prefixing it with /proc/. Easy. As usual, for the details refer to the gist.

A Shared Object

There is another piece of code. This one provides some services, and is built as a Linux shared object. It holds a pointer to a class Resource, which in turn holds a dictionary. It comprises two classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Resource;

class Provider {
Resource* r_;

public:
Provider();
std::string GetResource(const std::string& name);
};

class Resource {
map<string, string> dict_;

public:
Resource();
std::string GetName(const std::string& surname);
};

In this case, the resource is a dictionary (instead of the /proc/version file).

We want to connect these 2 pieces and make them independently compilable, meaning that the changes made either one should not affect the other. We want to be able, in particular, to recompile the shared object without recompiling the program, and have the program reflect the change in the shared object; and vice versa, a change in the program not affecting the shared object.

Whimsical as it might seem, this exercise has been inspired by an actual need I had in a real scenario. We want to be able to have old programs, that used to ask services to a library, now ask the same services to our own component. The complete code for the shared object can be found here.

The solution: Adding an interface and Using Dynamic Linking

The way I found to solve the problem is the following. Imagine you have another header file, that works as an interface. The client code only needs to know the interface; it will be in charge of asking the service to the provider. But to make the provider independent from the interface, we have to load the shared object at run-time, with dynamic linking and the good old dlopen. Now, since the provider is a C++ class, we export two C functions, create and destroy, to allow for, well, the creation and the destruction of an object of the class Provider. The creator, specifically, will only return a pointer, through which we’ll call the Provider‘s methods directly. I might be childish, but I found this almost poignant: we only use dlopen/dlclose/dlsym in the constructor and destructor of the interface, and nowhere else. No need to dlsym every function (after having to export it). We do not even need to declare the extern "C" creation/destruction functions in the header of the provider: we only write them in its implementation file.

Please have a look at what changed in the Provider’s implementation: before, as a stand-alone component, it was:

1
2
3
4
5
6
7
8
9
10
11
12
#include "provider.hpp"
#include "resource.hpp"
#include <iostream>

Provider::Provider() : r_(new Resource()) {
std::clog << "Provider::Provider()\n";
}

std::string Provider::GetResource(const std::string& name) {
std::clog << "Provider::GetResource()\n";
return r_->GetName(name);
}

Now, we only need to add:

1
2
3
4
5
6
7
8
9
extern "C" {
Provider* create() {
return new Provider;
}

void destroy(Provider* p) {
delete p;
}
}

Pretty unobtrusive, ain’t it?

The Star of the Show

Of course we did not forget the interface that makes all this possible:

1
2
3
4
5
6
7
8
9
10
11
class Provider;

class ProviderInterface {
Provider* impl_;
void* so_handle_;

public:
ProviderInterface();
string GetResource(const string& name);
~ProviderInterface();
};

implemented as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ...
#include <dlfcn.h>

ProviderInterface::ProviderInterface() {
clog << "ProviderInterface::ProviderInterface()\n";
so_handle_ = dlopen("./provider_module/libprovider.so", RTLD_LAZY);
char* error = dlerror();
if (error) {
std::cout << error << "\n";
exit(EXIT_FAILURE);
}

dlerror();
Provider* (*get_provider)();
get_provider = (Provider* (*)())dlsym(so_handle_, "create");
error = dlerror();
if (error) {
std::cout << error << "\n";
exit(EXIT_FAILURE);
}

impl_ = get_provider();
}

string ProviderInterface::GetResource(const string& name) {
clog << "ProviderInterface::GetResource()\n";
return impl_->GetResource(name);
}

ProviderInterface::~ProviderInterface() {
clog << "ProviderInterface::~ProviderInterface()\n";
dlerror();
void (*destroy_provider)(Provider*);
destroy_provider = (void (*)(Provider*))dlsym(so_handle_, "destroy");
char* error = dlerror();
if (error) {
std::cout << error << "\n";
exit(EXIT_FAILURE);
}

destroy_provider(impl_);
dlclose(so_handle_);
}

and a typical usage in a main program:

1
2
3
4
5
6
7
8
int main() {
ProviderInterface* iface(new ProviderInterface);
string result = iface->GetResource("Liskov");
cout << "Result is:\n\"\"\"";
cout << result;
cout << "\"\"\"";
return 0;
}

Needless to say, this time the resource will not be the content of a file, but the name of Doctor Liskov, Turing Award winner.

The Two Tests: A Conclusion

Now, if you believe me, you can separately compile the main program and the shared library; if you do not, just download the git project and play with it.

Do [sic] try this at home!