Знакомство с PDL (Portable Dynamic Loader) (исходники)

Источник: ProgZ
garik

Что такое PDL

PDL (portable dynamic loader) - это легкая, простая и портабельная библиотека, предназначенная для создания и использования динамически загружаемых объектов классов.

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

Основное применение этой технологии - это разработка динамически подключаемых плагинов, расширяющих функциональность основной программы. Главная проблема состоит в том, что динамически загружаемые библиотеки на многих платформах качественно поддерживают только процедурный стиль программирования, а с загрузкой и использованием экземпляров классов возникают разнообразные проблемы. Большинство этих проблем (но, к сожалению, пока не все) и решает библиотека 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
     */
    virtual const char * getclassname() const throw() = 0;
    
    /**
     * @brief destroy class instance
     */
    void destroy() throw() { delete this; }

protected:
    /**
     * @brief destructor
     */
    virtual ~dynamicclass() throw() {;; }
};

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

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

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

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

Код
#define declare_dynamic_class( classname ) \
public: \
    virtual const char * getclassname() const throw() { return #classname; }

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

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

Код
#include

сlass mytestinterface : public pdl::dynamicclass
{
public:
    /**
     * @brief test method
     */
    virtual void dosomething() throw() = 0;

    /**
     * @brief declare this class dynamically loadable
     */
    declare_dynamic_class( mytestinterface )
};

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

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

Код
#include
#include

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 { return new 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
#include

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 не решает проблемы с различным декорированием имён методов различными компиляторами - такая задача в настоящий момент не ставится.

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

  • Freebsd 6.2
  • Debian 4.0 linux 2.6.18-4
  • Opensuse 10.2
  • Windows xp

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

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=7646