
DynLib стала неотъемлемым инструментом разработки. Под катом делимся результатами.
Недостатки традиционного подхода к реализации DLL
К основным недостаткам традиционного подхода (реализации) можно отнести:- отсутствие возможности использовать пространства имен
- большое количество служебного кода, необходимого:
- при реализации динамической загрузки библиотек;
- при реализации межмодульного взаимодействия через классы, за счет использования декскрипторов (или иных неявных структур) и классов-оберток;
- при реализации механизмов возвращения ошибки, в случае, когда экспортируемые функции могут генерировать исключения.
Примеры использования 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/include.hpp>
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. Для подключения DLL к приложению необходимо включить представленный заголовочный файл test_lib.hpp в исходный код. Далее следует создать объект класса test::lib. Доступ к экспортируемых функциям DLL возможен через '.' или '->'. //========== 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\include.hpp>
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 <iostream>
#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. Использование исключений.
Задача. Функция вычисления квадратного корня sqrt в библиотеке calculator.dll должна возвращать ошибку в случае некорректного входного значения.
Решение
//========== calculator.hpp ==========
#include <dl\include.hpp>
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 <iostream>
#include <locale>
#include "calculator.hpp"
int main()
{
using namespace std;
locale::global( locale("", locale::ctype) );
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 по работе с геометрическими фигурами (квадрат, прямоугольник, круг). Все фигуры должны поддерживать общий интерфейс, через который можно узнать координаты центра фигуры.
Решение
//========== shapes.hpp ==========
#include <dl/include.hpp>
DL_NS_BLOCK(( shapes )
(
DL_INTERFACE(figure)
(
( char const*, name, )
( double, center_x, )
( double, center_y, )
( void, center_xy, (double&,x)(double&,y) )
)
))
DL_NS_BLOCK(( shapes )
(
DL_LIBRARY(lib)
(
( shapes::figure, create_rectangle, (double,left)(double,top)(double,width)(double,height) )
( shapes::figure, create_square, (double,left)(double,top)(double,size) )
( shapes::figure, create_circle, (double,center_x)(double,center_y)(double,radius) )
)
))
//========== shapes_lib.cpp ==========
#include "shapes.hpp"
class rectangle
{
public:
rectangle(double l, double t, double w, double h)
: l_(l), t_(t), w_(w), h_(h)
{
if (w < 0)
throw std::invalid_argument( "неверно задана ширина прямоугольника" );
if (h < 0)
throw std::invalid_argument( "неверно задана высота прямоугольника" );
}
char const* name() { return "rectangle"; }
double center_x() { return l_ + w_ / 2.; }
double center_y() { return t_ + h_ / 2.; }
void center_xy(double& x, double& y) { x = center_x(); y = center_y(); }
private:
double l_, t_, w_, h_;
};
class square
{
public:
square(double l, double t, double s)
: l_(l), t_(t), s_(s)
{
if (s < 0)
throw std::invalid_argument( "неверно задана длина стороны квадрата" );
}
char const* name() { return "square"; }
double center_x() { return l_ + s_ / 2.; }
double center_y() { return t_ + s_ / 2.; }
void center_xy(double& x, double& y) { x = center_x(); y = center_y(); }
private:
double l_, t_, s_;
};
class circle
{
public:
circle(double x, double y, double r)
: x_(x), y_(y), r_(r)
{
if (r < 0)
throw std::invalid_argument( "неверно задан радиус круга" );
}
char const* name() { return "circle"; }
double center_x() { return x_; }
double center_y() { return y_; }
void center_xy(double& x, double& y) { x = x_; y = y_; }
private:
double x_, y_, r_;
};
struct shapes_lib
{
static shapes::figure create_rectangle( double l, double t, double w, double h )
{
return dl::shared<rectangle>( l, t, w, h );
}
static shapes::figure create_square( double l, double t, double s )
{
return dl::shared<square>( l, t, s );
}
static shapes::figure create_circle( double x, double y, double r )
{
return dl::shared<circle>( x, y, r );
}
};
DL_EXPORT( shapes::lib, shapes_lib )
//========== application.cpp ==========
#include <iostream>
#include "shapes_lib.hpp"
void print_center( shapes::figure shape )
{
std::cout << shape.name() << ": " << shape.center_x() << "-" << shape.center_y() << std::endl;
}
int main()
{
shapes::lib lib( "shapes.dll" );
print_center( lib.create_circle(10, 10, 10) );
print_center( lib.create_square(0, 0, 20) );
print_center( lib.create_rectangle(0, 5, 20, 10) );
return 0;
}
Как подключить библиотеку
Библиотека поставляется в виде заголовочных файлов. Никаких .lib и .dll не требуется. Для подключения требуется добавить следующую директиву:
#include <dl/include.hpp>
Элементы библиотеки
Многие классы и макросы библиотеки DynLib могут использоваться самостоятельно и отдельно друг от друга.
DL_BLOCK
Служит контейнером для всех остальных макросов.
DL_BLOCK
(
// declarations
)
DL_NS_BLOCK
Служит контейнером для всех остальных макросов. Создает пространства имен для класса.
DL_NS_BLOCK( (ns0, ns1, ns2 … )/*пространства имен, до 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_RECORD
Макрос DL_RECORD генерирует упакованную структуру данных для использования в межмодульном взаимодействии. Дополнительно создается конструктор со всеми перечисленными в макросе аргументами.
DL_RECORD( record_name )
(
/*fields*/
(type, name, =default_value)
)
Пример:
//========== some_exe.cpp ==========
#include <dl/include.hpp>
DL_BLOCK
(
DL_RECORD( data )
(
( int, x )
( int, y, = 100 /*значение по умолчанию*/ )
( int, z, = 200 /*значение по умолчанию*/ )
)
)
int main()
{
data v( 20 ); //инициализация x = 20, y = 100, z = 200
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/include.hpp>
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 <test1_lib.hpp>
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 — полное имя класса класса, РЕАЛИЗУЮЩЕГО функции, указанные в интерфейсе взаимодействия.
- Создать класс (структуру);
- Определить каждую функцию из интерфейса как статическую. Функции должны находиться в области видимости public:;
- Произвести экспорт функций, написав конструкцию DL_EXPORT(lib, impl).
//========== 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::ref
int tcount = p->threads_count();
p.process(some_buf, some_buf_size);
dl::shared
Шаблонный класс dl::shared<T> решает следующие задачи:
- динамическое создание объекта класса T с аргументами, переданными в конструкторе;
- добавление счетчика ссылок и обеспечение разделяемого владения (подобно boost(std)::shared_ptr);
- неявное приведение к объекту класса, генерируемого макросом DL_INTERFACE.
Классы 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<my_processor> p1( "some processor name" );
// объект класса my_processor создается динамически
dl::shared<my_processor> p2;
// объект класса my_processor создается динамически c конструктором по умолчанию
dl::shared<my_processor> p3( p1 );
// p3 и p1 ссылаются на один и тот же объект, счетчик ссылок = 2
dl::shared<my_processor> 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<my_processor>
return 0;
}
Поддерживаемые компиляторы
Библиотека DynLib полностью совместима со следующими компиляторами (средами разработки):
- Microsoft Visual C++ 2008;
- Microsoft Visual C++ 2010;
- MinGW GCC 4.5.0 и выше.
- CodeGear С++ Builder XE (не гарантируется работа при определенных настройках компилятора)