понедельник, 11 ноября 2013 г.

Динамические библиотеки под Linux на С++.

Для того, чтобы действительно научиться работать с какой-то технологией, по ней надо рассказать. Этим я сегодня и займусь. Итак. Буду я писать, как пользоваться динамическими библиотеками в C++ на GNU Linux. Использовать буду компилятор g++.
А писать мы будем библиотечку, реализующую какое-нибудь простенькое шифрование.




Система директорий

Для начала давайте поймём, что такое динамическая библиотека. Итак, динамическая библиотека - бинарный файл, в котором реализованы некоторые функции, а может даже содержатся целые классы. В нашем случае будет содержаться класс. Если в библиотеке содержится класс, то программе которая будет использовать эту библиотеку понадобится описание этого класса, или хотя бы описание интерфейса, который этот класс реализует. Давайте для начала создадим такие каталоги:
├── build
│   └── plugins
├── CoreProgram
├── dest
│   └── plugins
└── Plugins
    ├── common
    └── public
Команда: mkdir -p CoreProgramm build/plugins dest/plugins Plugins/{common,public}

В каталог build будет происходить компиляция основной программы, в его подкаталог plugins будет происходить компиляция плагинов

В каталог dest будет помещаться исполняемый файл основной программы, а в его подкаталог plugins будут помешаться библиотеки. 

В каталоге CoreProgram будет находиться исходный код основной программы.

В каталоге Plugins будет находиться исходный код плагинов.

В подкаталоге Plugins/public будет содержаться интерфейс библиотеки. А именно - виртуальный класс, который определяет набор функций, которые должна реализовать библиотека. Я поместил его в каталог public, так как о нём должна знать как библиотека так и программа её использующая.

В подкаталоге Plugins/common будут содержаться файлы,общие для всех плагинов, но которые не должны включатся в основную программу. 

Определение интерфейса

Для того,чтобы писать библиотеку мы определим интерфейс по которому она будет работать. Прелесть подобного подхода в том, что позже. когда мы захотим обновить нашу программу, а точнее реализацию некоторых функций внутри библиотеки, мы можем перекомпилировать только библиотеку, а программа останется нетронутой. Мы можем кардинально изменить её логику работы, сохранив интерфейс, и программа даже не заметит подмены. К слову, этой лазейкой часто пользуются различные нехорошие люди для заражения ваших компьютеров.  

Итак, библиотека будет шифровать текстовые данные. Если точнее, она будет принимать на входе текстовую строку и возвращать текстовую строку, и инициализироваться будет с помощью файла, в котором будут задаваться некоторые параметры, для каждой реализации библиотеки свои. Иными словами, вот интерфейс нашей библиотеки:
 
#ifndef PLUGIN_INTERFACE
 #define PLUGIN_INTERFACE
#include <string>
#include <fstream>
#include <dlfcn.h>
#include <stdlib.h>
#include <iostream>
using namespace std;


class PluginInterface
{
public:
 virtual int init(const string& filename) = 0;
 virtual string crypt(const string& data) = 0;
 virtual string decrypt(const string& data) = 0;
 virtual ~PluginInterface()=0;
};

PluginInterface* getInstance(void* lib)
{
 PluginInterface*(*getinstance_f)()  = (PluginInterface*(*)())dlsym(lib, "create");
 PluginInterface* instance = getinstance_f();
 return instance;
}
#endif
Этот файл мы сохраним в Plugins/public/plugin_interface.h. Это описание структуры библиотечного класса и функция, возвращающая этот класс из сообщенной в неё библиотеки.
Возвращает она библиотеку путём вызова функции create из этой библиотеки. А библиотекописатель уже должен позаботиться, чтобы у него была функция create и чтобы эта функция была extern "C", чтоб её можно было вызвать c помощью ldsym.

Добавление функционала, единого для всех библиотек. 

Перед тем, как реализовывать интерфейс, определим, некоторые общие правила для всех библиотек. Первое, конечно - интерфейс, который необходимо наследовать. Ещё пусть все библиотеки будут принимать конфигурационный файл в формате JSON. Это значит, что все библиотеки должны уметь его парсить. Класть в каждую библиотеку по набору классов, для работы с JSON не умно. Поможет нам уже созданный каталог common. В этот каталог мы поместим набор классов для парсинга JSON. На просторах интернета я нашел замечательную легковесную библиотеку vjson. Она полностью нам подходит. Теперь каталог common выглядит так:

├── block_allocator.cpp
├── block_allocator.h
├── json.cpp
├── json_doc.h
├── json.h
├── utils.cpp
└── utils.h

Файлы utils*  я  создал самостоятельно. В них определил функцию find, которая способна по тектовому ключу найти тектовое значение в json файле.

Реализация интерфейса библиотеки


Для этого создадим  каталог SimpleCrypt в Plugins и создадим там файл. simple_crypt.h
и simple_crypt.cpp.
Вот содержание simple_crypt.h:

#ifndef SIMPLE_CRYPT_H
 #define SIMPLE_CRYPT_H

#include "../public/plugin_interface.h"
#include "../common/json_doc.h"
#include "../common/utils.h"
using namespace std;


class SimpleCrypt : public PluginInterface
{

public:
 SimpleCrypt(){};
 int init(const string& filename);
 string crypt(const string& data);
 string decrypt(const string& data);
 PluginInterface() { delete this->json;}
 JsonDocument* json;
private:
 char replace(char inp,json_value*vlocabruary);
};


extern "C" PluginInterface* create()
{
 return new SimpleCrypt();
}

#endif

Как видите, всего-то реализация интерфейса PluginInterface c добавленной одной служебной функцией, создающей экземпляр класса SimpleCrypt. Но самый сок внутри simple_crypt.cpp 


#include "simple_crypt.h"
#include <iostream>


int SimpleCrypt::init(const string& filename)
{  
 this->json = new JsonDocument();
 int res = this->json->parse(filename.c_str());
 return 1;
}

string SimpleCrypt::crypt(const string& data)
{
 json_value* volcabruary = this->json->root()->first_child->first_child;
 const char* data_c = data.c_str();
 char* result = new char[data.size()+1];
 for(int i = 0;i<data.size();i++)
 {
  char c = data_c[i];
  
  char rep = replace(c,volcabruary);
  if(rep!='\0')
   result[i]=rep;
  else
   result[i] = ' ';
 }
 result[data.size()]='\0';
 return string(result);
}

string SimpleCrypt::decrypt(const string& data)
{
 json_value* volcabruary = this->json->root()->first_child->first_child->next_sibling;
 const char* data_c = data.c_str();
 char* result = new char[data.size()];
 
 for(int i = 0;i<data.size();i++)
 {
  char c = data_c[i];
  char rep = replace(c,volcabruary);
  if(rep!='\0')
   result[i]=rep;
  else
   result[i] = ' ';
 }
 
 return string(result);
}
char SimpleCrypt::replace(char inp,json_value* vlocabruary)
{
 char* temp = new char[2];
 sprintf(temp,"%c\0",inp);
 char*result = 0;
 find(temp,vlocabruary,&result);
 if(result)
 {
  char res = result[0];
  delete temp;
  return res;
 }
 return '\0';
}



Функционал очень простой. Библиотека читает JSON файл, находит в нём соответствия между символами и производит замену. 


Компиляция библиотеки:

Компилировать библиотеку я буду из каталога build/plugins/SimpleCrypt и выглядеть это будет так 

g++ -shared -fPIC ../../../Plugins/SimpleCript/simple_crypt.cpp ../../../Plugins/common/*.cpp ../../../Plugins/public/*.h -o ../../../dest/plugins/libSC.so

С ключ -shared указывает на то, что это будет shared library, а fPIC говорит нам о  независимости разсоложения в памяти исполняемого кода. Это обязательно нужно для динамической библиотеки, так как она может поместиться во время выполнения куда угодно. Подробней про fPIC прочесть можно тут http://stackoverflow.com/questions/5311515/gcc-fpic-option . 
Результатом стал libSC.so файл в каталоге /dest/plugins.

Написание клиента и тестирование библиотеки:

Самое важное - это чтобы библиотека работала. Для тестирования этого напишем такого клиента.

#include "../Plugins/public/plugin_interface.h"
#include <iostream>
int main(int argc,char**argv)
{
 void * lib;;
 lib = dlopen(argv[1], RTLD_LAZY);
 if (!lib) {
      printf("cannot open library '%s'n", argv[1]);
      return 1;
    }
 PluginInterface* cript = getInstance(lib);
 cript->init(argv[2]);
 std::string love("love you");
 std::string cripted = cript->crypt(love);
 std::cout<<love<<std::endl;
 std::cout<<cripted<<std::endl;
 return 0;
}


У неё простая логика. В качестве первого параметра принимается файл библиотеки, вторым параметром передаётся json файл с параметрами. Клиент извлекает из библиотеки реализацию класса, который уже займётся простейшим шифрованием. Вот и всё.
Выполняется это так:

dest $ ./prog ./plugins/libSC.so ./test1.json 
love you
ikrb xkz

А вот так выглядит конечное дерево проекта:

├── build
│   ├── build.sh
│   └── plugins
│       └── SimpleCrypt
│           └── build.sh
├── CoreProgramm
│   └── main.cpp
├── dest
│   ├── plugins
│   │   └── libSC.so
│   ├── prog
│   └── test1.json
└── Plugins
    ├── common
    │   ├── block_allocator.cpp
    │   ├── block_allocator.h
    │   ├── json.cpp
    │   ├── json_doc.h
    │   ├── json.h
    │   ├── utils.cpp
    │   └── utils.h
    ├── public
    │   └── plugin_interface.h
    └── SimpleCript
        ├── simple_crypt.cpp
        └── simple_crypt.h

Надеюсь, это будет кому-то полезно или интересно.  

1 комментарий: