Pull to refresh

DynLib: библиотека для создания и работы с DLL

Reading time11 min
Views9.6K
image Библиотека DynLib предоставляет удобные средства для разработчиков, использующих межмодульное взаимодействие (EXE<->DLL, DLL<->DLL) в своих проектах, и значительно сокращает время и количество кода.
DynLib стала неотъемлемым инструментом разработки. Под катом делимся результатами.




Недостатки традиционного подхода к реализации DLL

К основным недостаткам традиционного подхода (реализации) можно отнести:
  1. отсутствие возможности использовать пространства имен
  2. большое количество служебного кода, необходимого:
    • при реализации динамической загрузки библиотек;
    • при реализации межмодульного взаимодействия через классы, за счет использования декскрипторов (или иных неявных структур) и классов-оберток;
    • при реализации механизмов возвращения ошибки, в случае, когда экспортируемые функции могут генерировать исключения.
Эти проблемы решаются с помощью библиотеки 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/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_C_LIBRARY, нельзя передавать через границы DLL
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 выполняет несколько задач:
  1. выступает в роли описания (документирования) интерфейса между EXE(DLL) и DLL;
  2. содержит необходимые структуры для автоматического экспорта функций библиотеки для разработчика;
  3. реализует класс, обеспечивающий загрузку DLL с заданным интерфейсом и предоставляющий доступ к экспортируемым функциям со стороны пользователя;
  4. обеспечивает корректное использование 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 — полное имя класса класса, РЕАЛИЗУЮЩЕГО функции, указанные в интерфейсе взаимодействия.
Для экспорта функций DLL необходимо:
  1. Создать класс (структуру);
  2. Определить каждую функцию из интерфейса как статическую. Функции должны находиться в области видимости public:;
  3. Произвести экспорт функций, написав конструкцию 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::ref
    int tcount = p->threads_count();
    p.process(some_buf, some_buf_size);

dl::shared

Шаблонный класс dl::shared<T> решает следующие задачи:
  1. динамическое создание объекта класса T с аргументами, переданными в конструкторе;
  2. добавление счетчика ссылок и обеспечение разделяемого владения (подобно boost(std)::shared_ptr);
  3. неявное приведение к объекту класса, генерируемого макросом 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<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 (не гарантируется работа при определенных настройках компилятора)
Взять библиотеку можно здесь
Tags:
Hubs:
Total votes 49: ↑41 and ↓8+33
Comments54

Articles