Библиотека 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 (не гарантируется работа при определенных настройках компилятора)
Взять библиотеку можно здесь