Programming modular applications in C++ (Part 1)

Programming

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:

 1 #ifndef MODULE_MANAGER_HPP
 2 #define MODULE_MANAGER_HPP
 3 #include <map>
 4 #include <string>
 5 #include <vector>
 6 
 7 class ModuleManager
 8 {
 9   public:
10     typedef double (*CalcFunc)(double, double);
11 
12   private:
13     std::map<std::string, CalcFunc> loaded_functions_;
14 
15   public:
16     std::vector<std::string> getFunctionsList();
17     CalcFunc getFunction(std::string);
18     bool addFunction(std::string, CalcFunc);
19     bool loadModule(std::string);
20 };
21 
22 #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 (I told about all these functions in 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 loades two functions from it: getName and function which name had been returned by getName.

This is implementation of above class:

 1 #include "module_manager.hpp"
 2 #include <dlfcn.h>
 3 
 4 std::vector<std::string> ModuleManager::getFunctionsList()
 5 {
 6   std::vector<std::string> list;
 7   for (auto &pair : loaded_functions_)
 8   {
 9     list.push_back(pair.first);
10   }
11   return list;
12 }
13 
14 ModuleManager::CalcFunc ModuleManager::getFunction(std::string key)
15 {
16   auto it = loaded_functions_.find(key);
17   if (it == loaded_functions_.end())
18   {
19     return nullptr;
20   }
21 
22   return it->second;
23 }
24 
25 bool ModuleManager::addFunction(std::string key, ModuleManager::CalcFunc func)
26 {
27   auto addition = loaded_functions_.insert(std::make_pair(key, func));
28   return addition.second;
29 }
30 
31 bool ModuleManager::loadModule(std::string fname)
32 {
33   void* module = dlopen(fname.data(), RTLD_LAZY);
34   if (!module)
35   {
36     return false;
37   }
38 
39   const char* (*name)() = reinterpret_cast<const char* (*)()>(dlsym(module, "getName"));
40   if (!name)
41   {
42     return false;
43   }
44 
45   double (*func)(double, double) = reinterpret_cast<double (*)(double, double)>(dlsym(module, name()));
46   if (func)
47   {
48     return addFunction(std::string(name()), func);
49   }
50 
51   return false;
52 }

Application

When we know how ModuleManager looks like, we can write an application which use it. I described dlfcn.h library in introductory article. Rest of code should be clear.

Now we can write application. Application will allow to:

  • load new module
  • call existing module
  • quit application

Each functionality will be called using proper command:

 1 #include <iostream>
 2 #include <cmath>
 3 #include <string>
 4 #include <vector>
 5 #include <sstream>
 6 
 7 #include "module_manager.hpp"
 8 
 9 double add(double, double);
10 double subtract(double, double);
11 
12 int main(int argc, char **argv)
13 {
14   ModuleManager manager;
15   manager.addFunction("add", add);
16   manager.addFunction("subtract", subtract);
17 
18   std::string selection;
19   do
20   {
21     auto names = manager.getFunctionsList();
22     for (auto &name : names)
23     {
24       std::cout << name << "\n";
25     }
26     std::cout << "\\load -- load new module\n";
27     std::cout << "\\quit -- quit application\n";
28     std::cin >> selection;
29 
30     if (selection == "\\load")
31     {
32       std::string path;
33       std::cout << "module path: ";
34       std::cin >> path;
35       if (manager.loadModule(path))
36       {
37         std::cout << "++ module loaded!\n";
38       } else
39       {
40         std::cerr << "-- error occured while loading module!\n";
41       }
42     } else if (selection != "\\quit")
43     {
44       auto func = manager.getFunction(selection);
45       if (func == nullptr)
46       {
47         std::cerr << "-- function does not exist!\n";
48       } else
49       {
50         double a, b;
51         std::cout << "arguments (2 doubles): ";
52         std::cin >> a >> b;
53         std::cout << "args: " << a << " " << b << "\n";
54 
55         std::cout << "++ result of " << selection << "(" << a << ", " << b << ") = " << func(a,b) << "\n";
56       }
57     }
58   } while (selection != "\\quit");
59 
60   return 0;
61 }
62 
63 double add(double a, double b)
64 {
65   return a + b;
66 }
67 
68 double subtract(double a, double b)
69 {
70   return a - b;
71 }

Now I'll tell you how it works. In line 14 we create object of ModuleManager and add to 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 in line 28. Next we do what is suitable for user's selection. 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 is as loaded function. We look for it in manager and call it with arguments which we received after asking user for them in line 52.

Multiplication and division modules

Now it's time for modules. They are very simple. Multiplication is in multiply_module.cpp:

1 extern "C" double multiply(double a, double b)
2 {
3   return a * b;
4 }
5 
6 extern "C" const char * getName()
7 {
8   return "multiply";
9 }

and division in divide_module.cpp:

1 extern "C" double divide(double a, double b)
2 {
3   return a / b;
4 }
5 
6 extern "C" const char * getName()
7 {
8   return "divide";
9 }

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:

1 g++ -std=c++11 module_manager.cpp -c -DDEBUG -o module_manager.o
2 g++ -std=c++11 calc.cpp module_manager.o -DDEBUG -ldl -o calc
3 g++ -fPIC -shared multiply_module.cpp -DDEBUG -o multiply_module.so
4 g++ -fPIC -shared divide_module.cpp -DDEBUG -o divide_module.so

This is simple (maybe not the simplest) way to do exercise I gave you in previous part of article. Now we are going to programme something more refined!

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.

As in the previous examples we have to write application and base module class. We will implement application as Shell class. main function will only call Shell's object member function.

shell_main.cpp:

 1 #include <iostream>
 2 #include <string>
 3 #include "shell.hpp"
 4 #include "shell_application.hpp"
 5 
 6 int main(int argc, char **argv)
 7 {
 8   if (argc != 2)
 9   {
10     std::cerr << "Usage: " << argv[0] << " prompt\n";
11     return 1;
12   }
13   Shell shell(argv[1]);
14   return shell.loop();
15 }

shell.hpp:

 1 #ifndef SHELL_HPP
 2 #define SHELL_HPP
 3 #include <string>
 4 #include <vector>
 5 #include <map>
 6 
 7 class ShellApplication;
 8 
 9 class Shell
10 {
11   typedef const char* (*GetNamePtr)();
12   typedef ShellApplication* (*LoadPtr)();
13   private:
14     const std::string prompt_;
15     const std::string load_name_;
16     std::map<std::string, LoadPtr> available_apps_;
17     std::vector<void*> apps_handlers_;
18 
19   public:
20     Shell(std::string = std::string("shell> "), std::string = std::string(":load"));
21     ~Shell();
22 
23     bool loadApplication(std::string);
24     int runCommand(std::string);
25     int loop();
26 
27   private:
28     std::vector<std::string> parseCommand_(std::string);
29 };
30 
31 #endif

shell.cpp:

  1 #include "shell.hpp"
  2 
  3 #include <dlfcn.h>
  4 #include <iostream>
  5 #include <sstream>
  6 #include <utility>
  7 
  8 #include "shell_application.hpp"
  9 
 10 Shell::Shell(std::string prompt, std::string load_name) :
 11   prompt_(prompt),
 12   load_name_(load_name)
 13 {
 14   available_apps_.insert(std::make_pair(load_name_, nullptr));
 15 }
 16 
 17 Shell::~Shell()
 18 {
 19   for (auto handler : apps_handlers_)
 20   {
 21     dlclose(handler);
 22   }
 23   apps_handlers_.clear();
 24 }
 25 
 26 bool Shell::loadApplication(std::string path)
 27 {
 28   void* handler = dlopen(path.data(), RTLD_LAZY);
 29   if (handler == 0)
 30   {
 31     std::cerr << "dl library error: " << dlerror();
 32     return false;
 33   }
 34 
 35   apps_handlers_.push_back(handler);
 36 
 37   GetNamePtr getName = reinterpret_cast<GetNamePtr>(dlsym(handler, "getName"));
 38   LoadPtr load = reinterpret_cast<LoadPtr>(dlsym(handler, "load"));
 39   if (!(getName && load))
 40   {
 41     std::cerr << "dl library error: " << dlerror();
 42     return false;
 43   }
 44 
 45   auto insertion = available_apps_.insert(std::make_pair(getName(), load));
 46   if (insertion.second)
 47   {
 48     std::clog << getName() << " application loaded!\n";
 49   }
 50   return insertion.second;
 51 }
 52 
 53 int Shell::runCommand(std::string cmd)
 54 {
 55   std::vector<std::string> cmd_parts = parseCommand_(cmd);
 56   if (cmd_parts.size() == 0)
 57   {
 58     std::cerr << "invalid command: " << cmd << "\n";
 59     return -1;
 60   }
 61   std::string& app_name = cmd_parts[0];
 62 
 63   if (app_name == load_name_)
 64   {
 65     std::cout << "Loading apps...\n";
 66     int result = 0;
 67     for (auto it = cmd_parts.begin() + 1; it != cmd_parts.end(); ++it)
 68     {
 69       std::cout << " + " << *it << " ";
 70       if (loadApplication(std::string("./") + *it + std::string("_application.so")))
 71       {
 72         std::cout << "[ OK ]\n";
 73       } else
 74       {
 75         std::cout << "\n";
 76         result = 1;
 77       }
 78     }
 79     return result;
 80   }
 81 
 82   auto app_load = available_apps_.find(app_name);
 83   if (app_load == available_apps_.end())
 84   {
 85     std::cerr << "could not find application: `" << app_name << "`\n";
 86     return -1;
 87   }
 88 
 89   ShellApplication* app = reinterpret_cast<ShellApplication*>(app_load->second());
 90   int result = app->runApplication(cmd_parts);
 91   delete app;
 92   return result;
 93 }
 94 
 95 int Shell::loop()
 96 {
 97   std::string cmd;
 98   std::cout << prompt_;
 99   while (std::getline(std::cin, cmd))
100   {
101     int status = runCommand(cmd);
102     std::cout << "Exit code: " << status << "\n" << prompt_;
103   }
104 
105   return 0;
106 }
107 
108 std::vector<std::string> Shell::parseCommand_(std::string cmd)
109 {
110   std::vector<std::string> tokens;
111   std::stringstream token;
112   const char delimiter = ' ', quote = '\"', escape = '\\';
113   bool escapeNext = false;
114   bool quoteMode = false;
115 
116   for (std::string::iterator it = cmd.begin(); it != cmd.end(); ++it)
117   {
118     char c = *it;
119     if (c == escape && escapeNext == false)
120     {
121       escapeNext = true;
122     } else if (c == quote && escapeNext == false)
123     {
124       quoteMode = !quoteMode;
125     } else if (c == delimiter && escapeNext == false && quoteMode == false)
126     {
127       if (token.str().empty() == false)
128       {
129         tokens.push_back(token.str());
130         token.str(std::string());
131       }
132     } else
133     {
134       token << c;
135       escapeNext = false;
136     }
137   }
138   if (token.str().empty() == false)
139   {
140     tokens.push_back(token.str());
141   }
142   return tokens;
143 }

Code discussion

Shell class has two std::string members. First is prompt_ - it contains prompt message and load_name_ which specify name of load function which is responsible for loading applications. They are set in Shell's constructor. We have also two containers: map of applications' names and pointers to their loaders (load functions) and vector of all resources (void pointers returned by dlopen function). Besides constructor and destructor Shell class has three public member functions: loadApplication, runCommand, loop. First loades shared library from given path, runCommand runs command passed by user and loop has while loop which asks user to type command and executes it. Shell has also one private member function called parseCommand_. It contains very simple algorithm which splits command into tokens. I'm going to write separate article about it for novices.

Shell assumes that we load applications from files located in current working directory. Command: :load myapp will seek file named: myapp_application.so in current directory.

Function loadApplication assumes that each shared module (shell's application) has two functions. First of them is getName which returns const char* with application's name. Second function initializes object of class which derives from ShellApplication (base class for all applications) and returns pointer to it.

Function runCommand uses parseCommand_ to split command into pieces, calls load function for this application and runs application's object runApplication function. I'll not clarify whole code. If you don't understand it, you can find explanation of each instruction from dlfcn.h in introductory article.

Each application returns int variable which corresponds to operating system's exit code. 0 means success. Other values can mean different types of errors.

Shell's applications

Application is represented by class which derives from ShellApplication base class. To make example clear ShellApplication will have only human-friendly name and pure virtual function runApplication. We'll also declare functions 'cpp:load and getName in ShellApplication's header file. Compiler won't shout if you don't define (implement) them in your application because applications are shared libraries, but declarations can remind programmer to write them. Without load and getName functions in application which is being tried to load, Shell will crash.

shell_application.hpp:

 1 #ifndef SHELL_APPLICATION_HPP
 2 #define SHELL_APPLICATION_HPP
 3 #include <string>
 4 #include <vector>
 5 
 6 class ShellApplication
 7 {
 8   private:
 9     std::string name_;
10 
11   public:
12     ShellApplication(std::string name) :
13       name_(name)
14     {}
15     virtual ~ShellApplication() {}
16 
17     std::string getName()
18     {
19       return name_;
20     }
21     virtual int runApplication(std::vector<std::string>&) = 0;
22 };
23 
24 extern "C" const char* getName();
25 extern "C" ShellApplication* load();
26 
27 #endif

Now we are ready to form our applications!

Echo and Concat applications

To present how our shell works, we should have at least one application. I wrote two similar examples: echo and concat. First of them prints all arguments on the screen separated with space. Concat concatenates all arguments and prints them.

echo_application.hpp:

 1 #include <iostream>
 2 #include "shell_application.hpp"
 3 
 4 class EchoApplication :
 5   public ShellApplication
 6 {
 7   public:
 8     EchoApplication() :
 9       ShellApplication("Echo")
10     {}
11 
12     int runApplication(std::vector<std::string>& argv)
13     {
14       for (auto it = argv.begin() + 1; it != argv.end(); ++it)
15       {
16         std::cout << *it << " ";
17       }
18       std::cout << "\n";
19       return 0;
20     }
21 };
22 
23 extern "C" const char* getName()
24 {
25   return "echo";
26 }
27 
28 extern "C" ShellApplication* load()
29 {
30   return new EchoApplication();
31 }

concat_application.hpp:

 1 #include <iostream>
 2 #include "shell_application.hpp"
 3 
 4 class ConcatApplication :
 5   public ShellApplication
 6 {
 7   public:
 8     ConcatApplication() :
 9       ShellApplication("Concatenate strings")
10     {}
11 
12     int runApplication(std::vector<std::string>& argv)
13     {
14       for (auto it = argv.begin() + 1; it != argv.end(); ++it)
15       {
16         std::cout << *it;
17       }
18       std::cout << "\n";
19       return 0;
20     }
21 };
22 
23 extern "C" const char* getName()
24 {
25   return "concat";
26 }
27 
28 extern "C" ShellApplication* load()
29 {
30   return new ConcatApplication();
31 }

Compilation

To compile whole application (shell) and shell's applications use this Makefile:

 1 CXX=g++
 2 CXXFLAGS=-std=c++11 -g -DDEBUG -Wall
 3 APPSFLAGS=-fPIC -shared
 4 OBJS=shell_main.o shell.o
 5 APPS=concat_application.so echo_application.so
 6 LIBS=-ldl
 7 TARGET=shell
 8 
 9 all: $(OBJS) $(APPS)
10   $(CXX) $(CXXFLAGS) $(OBJS) $(LIBS) -o $(TARGET)
11 
12 $(APPS): %so: %cpp
13   $(CXX) $(CXXFLAGS) $(APPSFLAGS) $< -o $@
14 
15 $(OBJS): %.o: %.cpp
16   $(CXX) $(CXXFLAGS) -c $< -o $@
17 
18 cleanup:
19   rm -f $(OBJS) $(TARGET) $(APPS)

To add new applications to Makefile, add their names in 5th line. Now you can load existing applications using :load command. To test it type these lines in your operating system's shell:

1 make && ./shell "prompt> "
2 prompt> :load echo
3 prompt> :load concat
4 prompt> echo text one two three
5 prompt> concat text ont two three "four five"

You can find all files from Shell example on my Gist.

Exercises

  1. Write some applications for shell. They can do simple math operations or create files.
  2. Add exit command to Shell. You should do it the same way as it has been done with :load command.
  3. (*) Try to add support for arrow keys. Up and down arrows allow to navigate in history of commands. Left and Right arrows allow to navigate in typed command. Tip: You will need termios.h library.

Read next part of this article!