Как экспортировать классы C++ из DLL - Как это работает

ОГЛАВЛЕНИЕ

Как это работает

Основой этого подхода является очень простая идея. Лишенный членов класс C++, состоящий из чисто виртуальных методов, является всего лишь виртуальной таблицей, т.е. массивом указателей функций. Этот массив указателей функций заполняется внутри DLL тем, чем автор считает нужным заполнить. Далее этот массив указателей используется вне DLL для вызова фактической реализации. Ниже приведена схема, иллюстрирующая использование интерфейса IXyz.

Нажмите на изображение, чтобы просмотреть полноразмерную схему в новом окне:

Схема выше показывает интерфейс IXyz, используемый модулями DLL и EXE. Внутри модуля DLL класс XyzImpl наследуется от IXyz и реализует его методы. Вызовы методов в модуле EXE вызывают фактическую реализацию в модуле DLL через виртуальную таблицу.

Почему это работает с другими компиляторами

Краткое объяснение: потому что технология COM работает с другими компиляторами. Теперь длинное объяснение. Использование лишенного членов абстрактного класса как интерфейса между модулями – именно то, что COM делает для предоставления интерфейсов COM. Понятие виртуальной таблицы из языка C++ отлично встраивается в спецификацию стандарта COM. Это не совпадение. Язык C++, будучи преобладающим языком разработки более 10 лет, широко применялся в программировании COM вследствие естественной поддержки объектно-ориентированного программирования в языке C++.

Неудивительно, что Microsoft счел язык C++ основным мощным инструментом для промышленной разработки COM. Будучи владельцем технологии COM, Microsoft позаботился о том, чтобы двоичный стандарт COM и их собственная реализация объектной модели C++ в компиляторе Visual C++ совпадали с минимальными издержками.

Естественно, что другие разработчики компиляторов C++ присоединились к победителям и реализовали формат виртуальной таблицы в своих компиляторах так же, как сделал Microsoft. Все хотели поддерживать технологию COM и обеспечить совместимость с существующим решением от Microsoft. Гипотетический компилятор C++, не поддерживающий COM эффективно, обречен на забвение на рынке Windows. Поэтому сейчас предоставление класса C++ из DLL через абстрактный интерфейс надежно работает во всех достойных компиляторах C++ на платформе Windows.

Использование интеллектуального указателя

Чтобы обеспечить правильное освобождение ресурса, абстрактный интерфейс предоставляет дополнительный метод для уничтожения экземпляра. Вызов этого метода вручную трудоемок и подвержен ошибкам. Эта ошибка широко распространена в сфере C, где разработчик должен не забыть освободить ресурсы путем явного вызова функции. Поэтому типичный код C++ использует идиому RAII(получение ресурса является инициализацией) с помощью интеллектуальных указателей. Проект XyzExecutable использует шаблон AutoClosePtr, предоставленный с примером. Шаблон AutoClosePtr является простейшей реализацией интеллектуального указателя, вызывающего произвольный метод класса для уничтожения экземпляра вместо operator delete. Ниже приведен фрагмент кода, демонстрирующий использование интеллектуального указателя вместе с интерфейсом IXyz:

#include "XyzLibrary.h"
#include "AutoClosePtr.h"

...
typedef AutoClosePtr<IXyz, void, &IXyz::Release> IXyzPtr;

IXyzPtr ptrXyz(::GetXyz());

if(ptrXyz)
{
    ptrXyz->Foo(42);
}

// Не нужно вызывать ptrXyz->Release(). Интеллектуальный указатель
// вызовет этот метод автоматически в деструкторе.

Использование интеллектуального указателя обеспечивает правильное освобождение объекта Xyz несмотря ни на что. Функция может завершиться досрочно из-за ошибки или внутреннего исключения, но язык C++ гарантирует, что деструкторы всех локальных объектов будут вызваны при завершении.

Использование стандартных интеллектуальных указателей C++

Последние версии MS Visual C++ предоставляют интеллектуальные указатели вместе со стандартной библиотекой C++. Ниже приведен пример использования объекта Xyz вместе с классом std::shared_ptr:

#include "XyzLibrary.h"

#include <memory>
#include <functional>

...

typedef std::shared_ptr<IXyz> IXyzPtr;

IXyzPtr ptrXyz(::GetXyz(), std::mem_fn(&IXyz::Release));

if(ptrXyz)
{
    ptrXyz->Foo(42);
}

// Не нужно вызывать ptrXyz->Release(). Класс std::shared_ptr
// вызовет этот метод автоматически в своем деструкторе.

Безопасность исключений

Так же, как интерфейсу COM не разрешено выдавать никакие внутренние исключения, абстрактный интерфейс C++ не вправе позволять никаким внутренним исключениям прорываться через границы DLL. Методы класса должны использовать коды возврата для указания ошибки. Реализация обработки исключений C++ очень специфичная для каждого компилятора и не может разделяться. Значит, в этом смысле абстрактный интерфейс C++ должен вести себя как простая функция C.

Плюсы
•    Экспортированный класс C++ используется через абстрактный интерфейс с любым компилятором C++.
•    Библиотеки времени выполнения C из DLL и из клиента независимы друг от друга. Так как получение и освобождение ресурса происходит полностью внутри модуля DLL, на клиента не влияет выбор CRT в DLL.
•    Достигается истинное выделение модуля. Полученный модуль DLL можно переработать и пересобрать, не влияя на остальной проект.
•    Модуль DLL легко преобразуется в полноценный модуль COM при необходимости.

Минусы
•    Требуется явный вызов функции для создания нового экземпляра объекта и для его удаления. Однако интеллектуальный указатель избавляет разработчика от второго вызова.
•    Метод абстрактного интерфейса не может возвращать или принимать обычный объект C++ в качестве параметра. Это должен быть встроенный тип (например, int, double, char*, и т.д.) или другой абстрактный интерфейс. Это же ограничение действует для интерфейсов COM.

Как насчет шаблонных классов STL?

Контейнеры стандартной библиотеки C++ (например, vector, list или map) и другие шаблоны не были спроектированы с расчетом на DLL. Стандарт C++ умалчивает о DLL, потому что это зависящая от платформы технология, не обязательно присутствующая на других платформах, где используется язык C++. Сейчас компилятор MS Visual C++ может экспортировать и импортировать создания экземпляров классов STL, которые разработчик явно помечает спецификатором __declspec(dllexport/dllimport). Компилятор выдает пару неприятных предупреждений, но это работает. Однако надо помнить, что экспорт созданий экземпляров шаблона STL ничем не отличается от экспорта обычных классов C++ со всеми сопутствующими ограничениями. В этом смысле нет ничего особенного с STL.

Резюме

Статья рассмотрела разные методы экспорта объекта C++ из модуля DLL. Дано подробное описание плюсов и минусов каждого метода. Описаны принципы безопасности исключений. Сделаны следующие выводы:
•    Плюс экспорта объекта в виде набора простых функций C в совместимости с широчайшим спектром сред разработки и языков программирования. Однако пользователю DLL приходится использовать устаревшие приемы C или обеспечивать дополнительные обертки над интерфейсом C, чтобы использовать современные парадигмы программирования.
•    Экспорт обычного класса C++ ничем не отличается от предоставления отдельной статической библиотеки с кодом C++. Применение очень простое и знакомое; но есть сильная связь между DLL и ее клиентом. Должна использоваться одна и та же версия одного и того же компилятора C++ и для DLL, и для ее клиента.
•    Объявление абстрактного класса без членов и его реализация внутри модуля DLL пока является лучшим подходом к экспорту объектов C++. Этот метод обеспечивает чистый, строго определённый объектно-ориентированный интерфейс между DLL и ее клиентом. Такая DLL может использоваться вместе с любым современным компилятором C++ compiler на платформе Windows. Использовать интерфейс вместе с интеллектуальными указателями почти столь же легко, как и использовать экспортированный класс C++.

Язык программирования C++ является мощным, универсальным и гибким инструментом разработки.