C++ / [Из песочницы] DynLib: библиотека для создания и работы с DLL

в 15:07, , рубрики: c plus plus, c++, dll, метки: , ,

C++ / [Из песочницы] DynLib: библиотека для создания и работы с DLL
Библиотека DynLib предоставляет удобные средства для разработчиков, использующих межмодульное взаимодействие (EXEDLL, DLLDLL) в своих проектах, и значительно сокращает время и количество кода.
DynLib была написана для внутреннего использования одним из наших сотрудников SergX и стала неотъемлемым инструментом разработки. Под катом делимся результатами.
Недостатки традиционного подхода к реализации DLL

К основным недостаткам традиционного подхода (реализации) можно отнести:отсутствие возможности использовать пространства имен

большое количество служебного кода, необходимого:при реализации динамической загрузки библиотек;

при реализации межмодульного взаимодействия через классы, за счет использования декскрипторов (или иных неявных структур) и классов-оберток;

при реализации механизмов возвращения ошибки, в случае, когда экспортируемые функции могут генерировать исключения.

Эти проблемы решаются с помощью библиотеки DynLib!
Примеры использования DynLib

1. Использование обычной DLL
Задача. Динамически подключить и использовать библиотеку test_lib.dll, реализующую простые математические операции, с интерфейсом, представленным в заголовочном файле:
//========== test_lib.h ==========#pragma once
 extern "C" __declspec(dllexport) int  __stdcall sum(int x, int y);extern "C" __declspec(dllexport) int  __stdcall mul(int x, int y);extern "C" __declspec(dllexport) double __stdcall epsilon();
Решение. Необходимо просто написать следующий заголовочный файл и подключить его к проекту.
//========== test_lib.hpp ==========#pragma once
#include

DL_NS_BLOCK( (test)
(
  DL_C_LIBRARY(lib)
  (
    ( int,   __stdcall, (sum), (int,x)(int,y))
    ( int,   __stdcall, (mul), (int,x)(int,y))
    ( double,__stdcall, (epsilon), () )
  )
))

Препроцессор сгенерирует класс test::lib, выполняющий динамическую загрузку DLL и содержащий перечисленные функции sum, mul и epsilon. Подключение реализации и использование класса тривиально: доступ к импортированным функциям осуществляется через объект класса посредством операторов '.' или '->'.
//========== exe.cpp ==========
#include "test_lib.hpp"int main()
{
  test::lib lib("path_to/test_lib.dll");
  int s = lib->sum(5, 20);
  int m = lib.mul(5, 10);
  double eps = lib.epsilon();
  return 0;
}

2. Создание библиотеки calculator.dll
Задача. Написать библиотеку calculator.dll, которая должна вычислять сумму, произведение и значение квадратного корня. Динамически загрузить библиотеку и вызвать каждую функцию.Решение
//========== calculator.hpp ==========
#include
DL_NS_BLOCK((team)
(
  DL_LIBRARY(calculator)
  (
    (double, sum, (double,x)(double,y))
    (double, mul, (double,x)(double,y))
    (double, sqrt, (double,x))
  )
))//========== calculator_dll.cpp ==========
#include "calculator.hpp"struct calculator
{
  static double sum(double x, double y) { return x + y; }
  static double mul(double x, double y) { return x * y; }
  static double sqrt(double x)     { return std::sqrt(x); }
};
DL_EXPORT(team::calculator, calculator)

Использование DLL
//========== application.cpp ==========
#include
#include "calculator.hpp"int main()
{
  using namespace std;
  team::calculator calc("calculator.dll");
  cout << "sum = " << calc.sum(10, 20) << endl;
  cout << "mul = " << calc.mul(10, 20) << endl;
  cout << "sqrt = " << calc.sqrt(25) << endl;
  return 0;
}

3. Модернизация библиотеки calculator.dll. Использование исключений.
Задача. Реализовать в calculator.dll механизм возвращения ошибки в случае некорректного входного значения для функции вычисления квадратного корня.Решение
//========== calculator.hpp ==========
#include
DL_NS_BLOCK((team)
(
  DL_LIBRARY(calculator)
  (
   (double, sqrt, (double,x))
  )
))//========== calculator_dll.cpp ==========
#include "calculator.hpp"struct calculator
{
  static double sqrt(double x)
  {
    if (x < 0)
      throw std::invalid_argument("значение аргумента меньше 0");
    return std::sqrt(x);
  }
};
DL_EXPORT(team::calculator, calculator)

Использование DLL
//========== application.cpp ==========
#include
#include
#include "calculator.hpp"int main()
{
  using namespace std;
  locale::global(locale(""));
  try
  {
    team::calculator calc("calculator.dll");
    cout << "sqrt1 = " << calc.sqrt(25) << endl;
    cout << "sqrt2 = " << calc.sqrt(-1) << endl;
  }
  catch (dl::method_error const& e)
  {
    cerr << "what:   " << e.what() << endl;
  }
  return 0;
}//========== результат выполнения ==========
sqrt1 = 5
what: exception 'class std::invalid_argument' in method 'sqrt' of class '::team::calculator' with message 'значение аргумента меньше 0'

4. Реализация библиотеки shapes.dll. Использование интерфейсов.
Задача. Создать библиотеку shapes.dll по работе с геометрическими фигурами (квадрат, прямоугольник, круг). Все фигуры должны поддерживать общий интерфейс, через который можно узнать координаты центра фигуры.РешениеИсходный код
Как подключить библиотеку

Библиотека поставляется в виде заголовочных файлов. Никаких .lib и .dll не требуется. Для подключения требуется добавить следующую директиву:
#include

Элементы библиотеки

Многие классы и макросы библиотеки DynLib могут использоваться самостоятельно и отдельно друг от друга.
DL_BLOCK

Служит контейнером для всех остальных макросов.
DL_BLOCK
(
  // declarations
)

DL_NS_BLOCK

Служит контейнером для всех остальных макросов. Создает пространства имен для класса.
DL_NS_BLOCK( (ns0, ns1, ns2 ... ns9)/*пространства имен, до 10*/
(
  // declarations
))

Макросы, которые описаны ниже кроме DL_EXPORT, должны быть помещены в DL_BLOCK или DL_NS_BLOCK
DL_C_LIBRARY

Назначение макроса — предоставить пользователю готовый класс, реализующий динамическую загрузку DLL и автоматический импорт функций. Макрос представлен как:
DL_C_LIBRARY(lib_class)
(
  /*functions*/
  (ret_type, call, (name, import_name), arguments)
)

lib_class — имя класса, реализацию которого генерирует библиотека DynLib;

functions — перечисление функций, экспортируемых DLL. задается через список следующего формата
(ret_type, call, (name, import_name), arguments)
ret_type — тип возвращаемого функцией значения;

call — формат вызова, например: __sdtcall, __cdecl и т.п.;

name — имя функции (для пользователя);

import_name — имя функции, заданной в таблице экспорта DLL, включая декорацию (если она есть). Если name и import_name совпадают, то import_name можно не указывать.

arguments — список (тип аргумента, имя аргумента, = значение по умолчанию), задающий входные аргументы. Имя аргумента и значение по умолчанию можно не указывать.;

Пример:
DL_BLOCK
(
  DL_C_LIBRARY(my_lib)
  (
   (void,  __stdcall, (func),       (int)(int,s)(double,V,=1.0) )
   (int,   __stdcall, (fn, "fn@0"), (int,a) )
   (int,   __stdcall, (fn),         () )
  )
)

.Классы, генерируемые макросом DL_C_LIBRARY, нельзя передавать через границы DLL
DL_RECORD

Макрос DL_RECORD генерирует упакованную структуру данных для использования в межмодульном взаимодействии. Дополнительно создается конструктор со всеми перечисленными в макросе аргументами.
DL_RECORD(record_name)
(
  /*fields*/
  (type, name, =default_value)
)

Пример:
//========== some_exe.cpp ==========
#include

DL_BLOCK
(
  DL_RECORD(data)
  (
    (int, x)
    (int, y, = 0 /*значение по умолчанию*/)
    (int, z, = 0 /*значение по умолчанию*/)
  )
)int main()
{
  data v(20);//инициализация x, т.к. нет значения по умолчанию
  v.x = 10;
  v.y = v.x;
  v.z = 50;
  v = data(5, 20, 30);
  data a(1, 2, 3);
  return 0;
}

DL_LIBRARY

Макрос DL_LIBRARY выполняет несколько задач:выступает в роли описания (документирования) интерфейса между EXE(DLL) и DLL;
содержит необходимые структуры для автоматического экспорта функций библиотеки для разработчика;
реализует класс, обеспечивающий загрузку DLL с заданным интерфейсом и предоставляющий доступ к экспортируемым функциям со стороны пользователя;
обеспечивает корректное использование C++ исключений:
- автоматический перехват C++ исключений на стороне DLL;
- возврат значения через границы DLL, сигнализирующего о наличии исключения;
- генерация нового исключения в случае, если на стороне DLL исключение было перехвачено (с восстановлением описания и информации о типе исключения).

DL_LIBRARY(name)
(
  /*functions*/
  (ret_type, name, arguments)
)
Классы, генерируемые макросом DL_LIBRARY, нельзя передавать через границы DLL
Для примера представим следующий заголовочный файл:
//========== test1_lib.hpp ==========#pragma once
#include

DL_NS_BLOCK( (team, test)
(
  DL_LIBRARY(lib)
  (
    (int,  sum,   (int,x)(int,y))
    (void,  mul,   (int,x)(int,y)(int&,result))
    (double, epsilon, ())
  )
))

Данное описание используется разработчиком DLL для экспорта функций посредством макроса DL_EXPORT. Пользователь, подключив заголовочный файл test1_lib.hpp, может сразу начать работу с DLL:
//========== test1_exe.cpp ==========
#include int main()
{
  team::test::lib lib("test1.dll");
  int s = lib.sum(5, 10);
  lib.mul(5, 5, s);
  double eps = lib->epsilon();
  return 0;
}

DL_EXPORT

Макрос DL_EXPORT предназначен для экспортирования функций DLL.DL_EXPORT(lib_class, lib_impl_class)
lib_class — полное имя класса, описывающего интерфейс взаимодействия (то имя класса, что использовалось в DL_LIBRARY);

lib_impl_class — полное имя класса класса, РЕАЛИЗУЮЩЕГО функции, указанные в интерфейсе взаимодействия.

Для экспорта функций DLL необходимо:Создать класс (структуру);
Определить каждую функцию из интерфейса как статическую. Функции должны находиться в области видимости public:;
Произвести экспорт функций, написав конструкцию DL_EXPORT(lib, impl).

Для примера, представим реализацию DLL для интерфейса взаимодействия в файле test1_lib.hpp, определенного в описании DL_LIBRARY.
//========== test1_dll.cpp ==========
#include "test1_lib.hpp"struct lib_impl
{
  static int sum(int x, int y)
  {
    return x + y;
  }
  static void mul(int x, int y, int& result)
  {
    result = x + y;
  }
  static double epsilon()
  {
    return 2.0e-8;
  }
};
DL_EXPORT(team::test::lib, lib_impl)

DL_INTERFACE

Макрос позволяет описать интерфейс класса и предоставить пользователю класс-обертку для работы с ним.
Реализация класса-обертки обеспечивает корректное использование C++ исключений:
- автоматический перехват C++ исключений на стороне DLL;
- возврат значения через границы DLL, сигнализирующего о наличии исключения;
- генерация нового исключения в случае, если на стороне DLL исключение было перехвачено (с восстановлением описания и информации о типе исключения).

Класс-обертка, генерируемая данным макросом, имеет разделяемое владение объектом, реализующего данный интерфейс. Разделяемое владение обеспечивается механизмом подсчета ссылок, т.е. когда происходит копирование объектов класса-обертки, вызывается внутренняя функция для увеличения счетчика ссылок, при уничтожении — внутренняя функция по уменьшению счетчика ссылок. При достижении счетчиком значения 0 происходит автоматическое удаление объекта. Доступ к методам интерфейса осуществляется через '.' или '->'.Библиотека DynLib гарантирует безопасное использование классов-интерфейсов на границе EXE(DLL)DLL
DL_INTERFACE(interface_class)
(
  /*methods*/
  (ret_type, name, arguments)
)

interface_class — имя класса, реализацию которого генерирует библиотека DynLib;

methods — перечисление функций, описывающих интерфейс класса,

Пример:
DL_NS_BLOCK( (example)
(
  DL_INTERFACE(processor)
  (  
    (int, threads_count, )
    (void, process, (char const*,buf)(std::size_t,size))
  )
))

Использование:
example::processor p;
p = ... // см. разделы dl::shared и dl::refint tcount = p->threads_count();
p.process(some_buf, some_buf_size);

dl::shared

Шаблонный класс dl::shared решает следующие задачи:динамическое создание объекта класса T с аргументами, переданными в конструкторе;
добавление счетчика ссылок и обеспечение разделяемого владения (подобно boost(std)::shared_ptr);
неявное приведение к объекту класса, генерируемого макросом DL_INTERFACE.

Доступ к членам-функциям класса T осуществляется через '->'.Классы dl::shared нельзя передавать через границы DLL.
Предположим, имеется класс my_processor и интерфейс example::processor:
class my_processor
{public:
  my_processor(char const* name = "default name");
  int threads_count();
  void process(char const* buf, std::size_t size);private:
  // состояние класса
};
DL_NS_BLOCK( (example)
(
  DL_INTERFACE(processor)
  ( 
   (int, threads_count, )
   (void, process, (char const*,buf)(std::size_t,size))
  )
))
:
Примеры использования dl::shared представлены ниже:
dl::shared p1("some processor name");// объект класса my_processor создается динамически
dl::shared p2;// объект класса my_processor создается динамически c конструктором по умолчанию
dl::shared p3(p1);// p3 и p1 ссылаются на один и тот же объект, счетчик ссылок = 2
dl::shared p4(dl::null_ptr);// p4 ни на что не ссылается
p3.swap(p4);// p4 ссылается на то же, что и p1, p3 - ни на что не ссылается
p4 = dl::null_ptr);// p4 ни на что не ссылается
p2 = p1;// p2 ссылается на объект p1
p2 = p1.clone();// создается копия объекта my_processor// в классе my_processor должен быть доступен конструктор копирования
p2->threads_count();
p2->process(/*args*/);// использование объекта my_processor
example::processor pi = p2;// приведение объекта my_processor к интерфейсу example::processor// pi также хранит ссылку на объект, и изменяет счетчик ссылок при создании, копировании и уничтожении.
pi->threads_count();
pi->process(/*args*/);
// использование объекта my_processor через интерфейс pi.

dl::ref

Функция библиотеки, позволяющая привести любой объект к объекту класса-интерфейса, объявленному через DL_INTERFACE, с идентичным набором методов. Обычно такое поведение необходимо, когда имеется функция, принимающая в качестве аргумента класс-интерфейс, а ему следует передать объект, размещенный в стеке.
Использовать функцию dl::ref нужно с осторожностью, поскольку объекты классов-интерфейсов, в этом случае не будут владеть переданными объектами, а управление временем жизни объекта и его использованием через классы-интерфейсы ложится на пользователя. Копирование объектов классов-интерфейсов, ссылающих на объекты, переданные через dl::ref, разрешено и вполне корректно (поскольку счетчика ссылок нет, то и изменять нечего — объекты классы-интерфейсов знают как здесь корректно работать).
class my_processor
{public:
  my_processor(char const* name = "default name");
  int threads_count();
  void process(char const* buf, std::size_t size);private:
  // состояние класса
};
DL_NS_BLOCK( (example)
(
  DL_INTERFACE(processor)
  (
  (int, threads_count, )
  (void, process, (char const*,buf)(std::size_t,size))
  )
))void some_dll_func(example::processor p)
{
  // использование p
}int main()
{
  my_processor processor("abc");
  some_dll_func(dl::ref(processor));
  // В качестве интерфейса выступает обычный объект класса, а не dl::object
  return 0;
}

Поддерживаемые компиляторы

Библиотека DynLib полностью совместима со следующими компиляторами (средами разработки):Microsoft Visual C++ 2008;
Microsoft Visual C++ 2010;
MinGW GCC 4.5.0 и выше.

Частично совместима со следующими компиляторами (средами разработки):CodeGear С++ Builder XE (не гарантируется работа при определенных настройках компилятора)
Взять библиотеку можно здесь


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js