Библиотека DynLib предоставляет удобные средства для разработчиков, использующих межмодульное взаимодействие (EXE<->DLL, DLL<->DLL) в своих проектах, и значительно сокращает время и количество кода.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, выполняющий динамическую загрузку DLL и содержащий перечисленные функции sum, mul и epsilon. Для подключения DLL к приложению необходимо включить представленный заголовочный файл test_lib.hpp в исходный код. Далее следует создать объект класса test::lib. Доступ к экспортируемых функциям DLL возможен через '.' или '->'.//========== 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), () ) ) ))
//========== 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 (не гарантируется работа при определенных настройках компилятора)
