Сообщений 0    Оценка 255        Оценить  
Система Orphus

Знакомство с PDL

(Portable Dynamic Loader)

Автор: Игорь Семёнов
Источник: RSDN Magazine #4-2007
Опубликовано: 15.03.2008
Исправлено: 10.12.2016
Версия текста: 1.1
Что такое PDL?
Для чего же нужна динамическая загрузка экземпляров классов?
Создание динамически загружаемого экземпляра класса
Использование динамически загружаемых классов
Ложка дёгтя
Благодарности
Ресурсы

Что такое PDL?

PDL (Portable Dynamic Loader) – это легкая, простая и переносимая библиотека, предназначенная для создания и использования динамически загружаемых экземпляров классов C++.

Для чего же нужна динамическая загрузка экземпляров классов?

Динамическая загрузка классов используется при разработке динамически подключаемых плагинов, расширяющих функциональность основной программы. Главная проблема состоит в том, что динамически загружаемые библиотеки на многих платформах качественно поддерживают только процедурный стиль программирования, а с загрузкой и использованием экземпляров классов возникают разнообразные проблемы. Большинство этих проблем (но, к сожалению, пока не все) и решает библиотека PDL.

На платформе Win32 PDL является сильно упрощённой альтернативой использованию технологии COM, без счётчика ссылок, без глобальной регистрации классов и множества других возможностей. Для платформ UNIX/Linux существуют аналогичные библиотеки, например, С++ Dynamic Class Loader. Поддержка динамической загрузки классов присутствует и в крупной кросс-платформенной библиотеке WxWidgets.

Целью разработки PDL было создание именно кроссплатформенной библиотеки, которая позволила бы использовать один и тот же код загрузки классов для платформ Win32 и UNIX/Linux (в отличие от COM и C++ Dynamic Class Loader). В то же время, библиотека должна была быть максимально независимой и легковесной (поэтому была забракована WxWidgets).

Создание динамически загружаемого экземпляра класса

Рассмотрим подробно процесс создания динамически загружаемого класса с использованием библиотеки PDL. Прежде всего, следует определить интерфейс класса, через который мы будем работать с экземпляром загруженного класса. Обязательное условие: этот интерфейс должен наследоваться от класса PDL::DynamicClass. Давайте повнимательнее присмотримся к определению DynamicClass:

сlass DynamicClass
{
public:
    /**
     * @brief Get class name
     * return class name
     */virtualconstchar * GetClassName() constthrow() = 0;
    
    /**
     * @brief Destroy class instance
     */void Destroy() throw() { deletethis; }

protected:
    /**
     * @brief Destructor
     */virtual ~DynamicClass() throw() {;; }
};

Чисто виртуальная функция GetClassName() возвращает имя класса. Вам не нужно беспокоиться о её определении, чуть ниже я объясню почему.

Невиртуальная функция Destroy() служит для уничтожения экземпляра класса. При этом сам деструктор сделан защищённым, чтобы предотвратить его прямой вызов. Зачем именно это сделано, будет также объяснено ниже.

Итак, интерфейс мы должны унаследовать от PDL::DynamicClass и определить в нём виртуальный деструктор, желательно в секции protected (чтобы не нарушать идеологию PDL).

Кроме того, в объявлении интерфейса обязательно нужно поместить вызов макроса DECLARE_DYNAMIC_CLASS, передав ему в качестве параметра имя класса. Если посмотреть на описание этого макроса, становится очевидно его предназначение: он просто определяет виртуальную функцию GetClassName():

      #define DECLARE_DYNAMIC_CLASS(className) \
public: \
    virtualconstchar * GetClassName() constthrow() { return #className; }

Ну и напоследок, добавим в описание интерфейса объявления чисто виртуальных методов, реализующих полезную функциональность будущих динамически загружаемых классов. Пусть в нашем случае это будет метод void DoSomething().

В итоге мы имеем следующее объявление интерфейса:

      #include <DynamicClass.hpp>

сlass MyTestInterface : public PDL::DynamicClass
{
public:
    /**
     * @brief Test method
     */virtualvoid DoSomething() throw() = 0;

    /**
     * @brief Declare this class dynamically loadable
     */
    DECLARE_DYNAMIC_CLASS(MyTestInterface)
};

Это объявление следует поместить в отдельный заголовочный файл, в нашем случае – MyTestInterface.hpp, потому как для обеспечения совместимости включаться он будет и при сборке динамически загружаемого класса, и при его непосредственной загрузке и использовании.

Далее следует определить сам класс, унаследовав его от абстрактного интерфейса MyTestInterface и определив методы, реализующие полезную функциональность. Кроме того, класс нужно экспортировать с помощью макроса EXPORT_DYNAMIC_CLASS. Обратите внимание, что этот макрос должен находиться вне определения класса:

      #include <MyTestInterface.hpp>
#include <stdio.h>

class MyTestClass1 : public MyTestInterface
{
public:
    /**
     * @brief Test method
     */void DoSomething() throw()
    {
        fprintf(stderr, "MyTestClass1::DoSomething()\n");
    }
};

EXPORT_DYNAMIC_CLASS(MyTestClass1)

Посмотрим на определение макроса EXPORT_DYNAMIC_CLASS (файл DynamicClass.hpp):

      #define EXPORT_DYNAMIC_CLASS(className) \
extern"C" PDL_DECL_EXPORT PDL::DynamicClass * Create##className() \
{ \
    try { returnnew className(); } \
    catch(...) {;; } \
    return NULL; \
}

Этот макрос объявляет и определяет экспортируемую функцию Create<имя_класса>, которая создаёт и возвращает экземпляр указанного в параметре класса. Модификатор extern “C” нужен для того, чтобы компилятор не декорировал имя функции в библиотеке. Макрос PDL_DECL_EXPORT объявляет функцию экспортируемой. Он определён в файле platform.h и его реализация зависит от конкретной платформы.

Следует обратить внимание на две вещи. Во-первых, в экспортируемой функции ловятся все исключения конструктора. Если вызов конструктора аварийно завершился выбросом исключения, функция просто вернёт NULL. Это связано со сложностями, возникающими при попытке обработать в основном коде исключение, генерируемое кодом плагина. Во-вторых, экспортируемая функция возвращает указатель на PDL::DynamicClass, к которому неявно преобразовывается созданный объект класса. Таким образом, если мы забудем унаследовать интерфейс создаваемого класса от PDL::DynamicClass, компилятор напомнит нам об этом.

После того, как класс создан, можно скомпилировать плагин. Следует отметить, что один плагин может содержать несколько различных динамически загружаемых классов, главное условие: они должны иметь уникальные в рамках этого плагина имена. Не следует забывать, что экспортировать при помощи макроса EXPORT_DYNAMIC_CLASS нужно каждый из классов.

Использование динамически загружаемых классов

Итак, у нас есть библиотека-плагин, содержащая динамически загружаемый класс. Теперь попробуем использовать этот класс.

Для начала потребуется экземпляр динамического загрузчика классов – PDL::DynamicLoader. Этот класс представляет собой синглтон, т.е. существует всегда в единственном экземпляре. Для получения ссылки на этот экземпляр используем статический метод DynamicLoader::Instance():

PDL::DynamicLoader & dynamicLoader = PDL::DynamicLoader::Instance();

Далее, нам нужно загрузить экземпляр класса и получить указатель на него:

MyTestInterface * instance =
    dynamicLoader.GetClassInstance< MyTestInterface >(myLibName, “MyTestClass1”);

Здесь myLibName – это имя библиотеки-плагина, например “MyTestClass1.dll” или “MyTestClass.so”. Не забываем подключить заголовочный файл с определением интерфейса класса – в нашем случае это MyTestInterface.hpp, упомянутый выше.

Наконец, вызываем метод загруженного класса:

instance -> DoSomething();

Так как динамический загрузчик в случае ошибки генерирует исключения типа PDL::LoaderException, будет правильно обернуть его вызовы в блок try/catch. Полный код примера будет выглядеть так:

      #include <MyTestInterface.hpp>
#include <stdio.h>

try
{
    PDL::DynamicLoader & dynamicLoader = PDL::DynamicLoader::Instance();
    MyTestInterface * instance =
        dynamicLoader.GetClassInstance< MyTestInterface >(myLibName, “MyTestClass1”);
    instance -> DoSomething();
}
catch(PDL::LoaderException & ex)
{
    fprintf(stderr, "Loader exception: %s\n", ex.what());
}

Следует отметить ещё один интересный момент: все динамически загруженные классы ведут себя как синглтоны. Иными словами, при повторном вызове DynamicLoader::GetInstance() с тем же именем библиотеки и с тем же именем класса будет возвращён указатель на уже загруженный экземпляр. Это сделано для облегчения контроля над удалением динамически загружаемых экземпляров классов. Если потребуется создавать множество экземпляров классов, рекомендуется реализовать фабрику классов и сделать её динамически загружаемой.

А как же удаляются экземпляры загруженных классов? Для этого и существует метод DynamicClass::Destroy(). Вызывать напрямую его не нужно – это делает сам загрузчик в своём деструкторе, либо при вызове метода DynamicLoader::Reset(). Почему же не вызывается обычный деструктор? Дело в том, что некоторые тонкости механизма выделения динамической памяти меняются от компилятора к компилятору. Представим себе, что плагин собран компилятором A, а основная программа, использующая его – компилятором B. Если мы загружаем класс из плагина, его экземпляр создаётся кодом, сгенерированным компилятором A. И если мы попытаемся удалить этот экземпляр из кода основной программы просто вызвав деструктор, то удалить его попытается код, сгенерированный компиляторов B. И тут возможны казусы.

Чтобы избежать подобных проблем, деструктор ~DynamicClass() сделан защищённым, а вместо него следует вызывать метод DynamicClass::Destroy(). Это метод гарантирует, что память выделяется и освобождается из одной и той же кучи.

Ложка дёгтя

Есть один тонкий момент, связанный с именами библиотек. Если имя библиотеки меняется, то считается, что это уже совершенно другая библиотека, хотя по-разному записанные имена могут указывать на одну и ту же библиотеку. Например: C:\MyProg\libs\mylib.dll и MyLIB.DLL.

Следует отметить также, что библиотека PDL не решает проблемы с различным декорированием имён методов различными компиляторами – такая задача в настоящий момент не ставится.

Текущая версия библиотеки не является thread-safe. Безопасное выполнение кода гарантируется только в рамках одного потока. В будущих версиях планируется исправить этот недостаток.

В настоящий момент библиотека оттестирована на платформах:

Я буду благодарен за любую информацию относительно работы PDL на других платформах.

Благодарности

Большое спасибо Владимиру Сторожевых (Влад), Александру Леденёву (Shunix) и Валерию Артюхину (Ikshot), благодаря чьей критике эта статья стала (я надеюсь) лучше.

Ресурсы


Эта статья опубликована в журнале RSDN Magazine #4-2007. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 255        Оценить