This article is from my old blog at kacperkolodziej.com.
In the Introduction to programming modular applications in C++ I described some simple examples. In this part I'm going to show you more sophisticated applications which also will use modules. Examples from this part will be object oriented and will present more professional approach to programming modular applications.
In this part I'll show you two examples. First would be a solution of task which I gave you at the end of last article. You were asked to write a calculator which can add numbers, subtract them and load modules with other operations. The second example I'm going to write for you will be simplified system shell (so-called command line).
Calculator
To simplify problem (for our purpose) we can assume that we add only operations which works on two real numbers (addition, subtraction, multiplication, division, modulo). So we cannot add factorial (one integer argument, not two real). In the second example you will be able to learn how to write more flexible application.
To begin with, we will write class called ModuleManager which will keep
modules' functions. Its header may look like this:
#ifndef MODULE_MANAGER_HPP
#define MODULE_MANAGER_HPP
#include <map>
#include <string>
#include <vector>
class ModuleManager
{
public:
typedef double (*CalcFunc)(double, double);
private:
std::map<std::string, CalcFunc> loaded_functions_;
public:
std::vector<std::string> getFunctionsList();
CalcFunc getFunction(std::string);
bool addFunction(std::string, CalcFunc);
bool loadModule(std::string);
};
#endif
To store functions we are using map from standard library. It keeps each element
as pair of key and that element. Elements are sorted by key. In this case key is
a string (also from standard library). It keeps function's name. The other
element of pair keeps pointer to function. We get this pointer using
dlsym function (described in the introductory article).
Function getFunctionsList returns vector of strings which contains names
of all loaded functions. We use getFunction to get pointer of function
related to name passed as an argument. addFunction adds function. If
function with key passed as an argument exists, addFunction returns
false. Otherwise it returns true. loadModule takes path to
binary file with compiled module and loads two functions from it:
getName and function which name had been returned by getName.
This is implementation of above class:
#include "module_manager.hpp"
#include <dlfcn.h>
std::vector<std::string> ModuleManager::getFunctionsList()
{
std::vector<std::string> list;
for (auto &pair : loaded_functions_)
{
list.push_back(pair.first);
}
return list;
}
ModuleManager::CalcFunc ModuleManager::getFunction(std::string key)
{
auto it = loaded_functions_.find(key);
if (it == loaded_functions_.end())
{
return nullptr;
}
return it->second;
}
bool ModuleManager::addFunction(std::string key, ModuleManager::CalcFunc func)
{
auto addition = loaded_functions_.insert(std::make_pair(key, func));
return addition.second;
}
bool ModuleManager::loadModule(std::string fname)
{
void* module = dlopen(fname.data(), RTLD_LAZY);
if (!module)
{
return false;
}
const char* (*name)() = reinterpret_cast<const char* (*)()>(dlsym(module, "getName"));
if (!name)
{
return false;
}
double (*func)(double, double) = reinterpret_cast<double (*)(double, double)>(dlsym(module, name()));
if (func)
{
return addFunction(std::string(name()), func);
}
return false;
}
Application
When we know how ModuleManager looks like, we can write an application which use it. Application will allow to:
- load new module
- call existing module
- quit application
#include <iostream>
#include <cmath>
#include <string>
#include <vector>
#include <sstream>
#include "module_manager.hpp"
double add(double, double);
double subtract(double, double);
int main(int argc, char **argv)
{
ModuleManager manager;
manager.addFunction("add", add);
manager.addFunction("subtract", subtract);
std::string selection;
do
{
auto names = manager.getFunctionsList();
for (auto &name : names)
{
std::cout << name << "\n";
}
std::cout << "\\load -- load new module\n";
std::cout << "\\quit -- quit application\n";
std::cin >> selection;
if (selection == "\\load")
{
std::string path;
std::cout << "module path: ";
std::cin >> path;
if (manager.loadModule(path))
{
std::cout << "++ module loaded!\n";
} else
{
std::cerr << "-- error occured while loading module!\n";
}
} else if (selection != "\\quit")
{
auto func = manager.getFunction(selection);
if (func == nullptr)
{
std::cerr << "-- function does not exist!\n";
} else
{
double a, b;
std::cout << "arguments (2 doubles): ";
std::cin >> a >> b;
std::cout << "++ result of " << selection << "(" << a << ", " << b << ") = " << func(a,b) << "\n";
}
}
} while (selection != "\\quit");
return 0;
}
double add(double a, double b)
{
return a + b;
}
double subtract(double a, double b)
{
return a - b;
}
In line 14 we create object of ModuleManager and
add to it functions: add and subtract. We do it without loading external code
because they are in application. Then we show all loaded functions and
additional commands: load and quit which can be used to load new module and
quit application. User makes choice from stdin. If he asked to load new module
we ask him for path to binary file with module. If he chose something different
from quit command, we treat it as loaded function. We look for it in manager
and call it with arguments which we received after asking user.
Multiplication and division modules
Now it's time for modules. They are very simple. Multiplication is in multiply_module.cpp:
extern "C" double multiply(double a, double b)
{
return a * b;
}
extern "C" const char * getName()
{
return "multiply";
}
and division in divide_module.cpp:
extern "C" double divide(double a, double b)
{
return a / b;
}
extern "C" const char * getName()
{
return "divide";
}
As you can see, we have two functions in our module. First is appropriate
function which does mathematical operation. Second one (getName) returns name of
first. It is used to load proper function used by calculator and to get
function's name for ModuleManager.
In the same way you can do modulo and other operations.
Compilation
You can compile whole program using these commands:
g++ -std=c++11 module_manager.cpp -c -DDEBUG -o module_manager.o
g++ -std=c++11 calc.cpp module_manager.o -DDEBUG -ldl -o calc
g++ -fPIC -shared multiply_module.cpp -DDEBUG -o multiply_module.so
g++ -fPIC -shared divide_module.cpp -DDEBUG -o divide_module.so
Your own shell!
Now I'm going to show you how to write your own command line. It will be a bit harder to code but its flexibility and capabilities will be greater.
We will implement the application as a Shell class. main function will only call
Shell's object member function.
shell_main.cpp:
#include <iostream>
#include <string>
#include "shell.hpp"
#include "shell_application.hpp"
int main(int argc, char **argv)
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " prompt\n";
return 1;
}
Shell shell(argv[1]);
return shell.loop();
}
shell.hpp:
#ifndef SHELL_HPP
#define SHELL_HPP
#include <string>
#include <vector>
#include <map>
class ShellApplication;
class Shell
{
typedef const char* (*GetNamePtr)();
typedef ShellApplication* (*LoadPtr)();
private:
const std::string prompt_;
const std::string load_name_;
std::map<std::string, LoadPtr> available_apps_;
std::vector<void*> apps_handlers_;
public:
Shell(std::string = std::string("shell> "), std::string = std::string(":load"));
~Shell();
bool loadApplication(std::string);
int runCommand(std::string);
int loop();
private:
std::vector<std::string> parseCommand_(std::string);
};
#endif
shell.cpp:
#include "shell.hpp"
#include <dlfcn.h>
#include <iostream>
#include <sstream>
#include <utility>
#include "shell_application.hpp"
Shell::Shell(std::string prompt, std::string load_name) :
prompt_(prompt),
load_name_(load_name)
{
available_apps_.insert(std::make_pair(load_name_, nullptr));
}
Shell::~Shell()
{
for (auto handler : apps_handlers_)
{
dlclose(handler);
}
apps_handlers_.clear();
}
bool Shell::loadApplication(std::string path)
{
void* handler = dlopen(path.data(), RTLD_LAZY);
if (handler == 0)
{
std::cerr << "dl library error: " << dlerror();
return false;
}
apps_handlers_.push_back(handler);
GetNamePtr getName = reinterpret_cast<GetNamePtr>(dlsym(handler, "getName"));
LoadPtr load = reinterpret_cast<LoadPtr>(dlsym(handler, "load"));
if (!(getName && load))
{
std::cerr << "dl library error: " << dlerror();
return false;
}
auto insertion = available_apps_.insert(std::make_pair(getName(), load));
if (insertion.second)
{
std::clog << getName() << " application loaded!\n";
}
return insertion.second;
}
int Shell::runCommand(std::string cmd)
{
std::vector<std::string> cmd_parts = parseCommand_(cmd);
if (cmd_parts.size() == 0)
{
std::cerr << "invalid command: " << cmd << "\n";
return -1;
}
std::string& app_name = cmd_parts[0];
if (app_name == load_name_)
{
std::cout << "Loading apps...\n";
int result = 0;
for (auto it = cmd_parts.begin() + 1; it != cmd_parts.end(); ++it)
{
std::cout << " + " << *it << " ";
if (loadApplication(std::string("./") + *it + std::string("_application.so")))
{
std::cout << "[ OK ]\n";
} else
{
std::cout << "\n";
result = 1;
}
}
return result;
}
// ... (find and run the app)
}
The shell implementation demonstrates a more flexible modular architecture
where each module is a full ShellApplication object, loaded dynamically
and invoked through a common interface.