Использование в Delphi классов, созданных в Visual C++ - Экспорт классов из DLL в Delphi

ОГЛАВЛЕНИЕ

Экспорт классов из DLL в Delphi

Передать, точнее, экспортировать несколько функций из DLL - не проблема. Приводим типы, соглашения о вызовах, заполняем список экспортируемых функций - и всё (в основном). Об этом написано немало, например, в [2], в параграфе "Использование DLL, разработанных в С++".

Экспортировать класс несколько сложнее. Даже если и DLL, и основная программа написаны на Delphi, возникают проблемы с распределением памяти, которые решаются использованием модуля ShаreMem, первым в списке uses как проекта, так и DLL [2, 3]. Причем, этот модуль можно, в принципе, заменить самодельным менеджером памяти [там же, 3]. Но как использовать ShаreMem, если DLL написана на другом языке, или написать собственный менеджер для двух языков? Наверное, можно и так, но, напоминаю, срок сдачи проекта - вчера. Если есть и другие возражения, часто время - определяющий фактор.

Можно создавать экземпляр класса при загрузке DLL, ликвидировать при выгрузке (используя события DLL_PROCESS_ATTACH/DETACH), а для методов класса (функций- членов, раз уж класс на С++) написать однострочные функции-обертки, не являющиеся членами, а просто вызывающие соответствующие функции-члены. Некрасиво, и много лишней работы. Попробуем все же экспортировать класс.

В [2], сказано: "Библиотеки DLL не могут экспортировать классы и объекты, если только вы не используете специализированную технологию Microsoft под названием СОМ или какую-либо другую усовершенствованную технологию". Впрочем, там же есть замечание: "На самом деле объекты могут быть переданы из DLL в вызывающую программу в случае, если эти объекты спроектированы для использования в качестве интерфейсов или чистых абстрактных классов". Кроме этого замечания, в [2] об экспорте объектов почти всё, но уже хорошо, что есть шанс "сделать это по-быстрому".

И, наконец, в [4] находим параграф "Экспорт объектов из DLL". Там сказано: "К объекту и его методам можно получить доступ, даже если этот объект содержится внутри DLL. Но к определению такого объекта внутри DLL и его использованию предъявляются определенные требования. Иллюстрируемый здесь подход применяется в весьма специфических ситуациях, и, как правило, такого же эффекта можно достичь путем применения пакетов или интерфейсов". Наша ситуация вполне специфическая; пакеты здесь неприменимы, так как они все же для использования с Delphi, про использование интерфейсов и СОМ уже сказано, а использовать интерфейсы без СОМ вне Delphi, судя по [2], нельзя.

И, пожалуй, самое важное из [4]:
"На экспорт объектов из DLL накладываются следующие ограничения:

  1. Вызывающее приложение может использовать лишь те методы объекта, которые были объявлены как виртуальные.
  2. Экземпляры объектов должны создаваться внутри DLL.
  3. Экспортируемый объект должен быть определен как в DLL, так и в вызывающем приложении с помощью методов, определенных в том же порядке.
  4. Из объекта, содержащегося вутри DLL, нельзя создать объект-потомок.

Здесь перечислены лишь основные ограничения, но возможны и некоторые другие. "

Далее там рассказывается о работе с DLL, написанной в Delphi, но полученной информации достаточно для работы с DLL, создаваемой в MS VC++.

Мастер MS VC++ позволяет создать обычную (regular) DLL и DLL-расширение (extension). Обычная DLL может экспортировать только С-функции и не способна экспортировать С++-классы, функции-члены или переопределенные функции [1]. Стало быть, надо использовать DLL-расширение. Мастер создаст заготовку, затем в каталог проекта надо будет скопировать два файла - заголовочный и файл кода (*.h и *.cpp), содержащие класс, с экземпляром которого предстоит поработать. Затем подключить их к проекту DLL и немного доработать в соответствии с указанными ограничениями.

Во-первых, все экспортируемые открытые методы (ну, или функции-члены, как хотите) необходимо объявить с директивой __stdcall, по понятным причинам. Во-вторых, их также необходимо объявить виртуальными. Это делается для того, чтобы точки входа оказались записанными в таблицу виртуальных функций (VMT), через которую и будет осуществлятся экспорт-импорт. В-третьих, класс требуется объявить с макроопределенной директивой AFX_EXT_CLASS или AFX_CLASS_EXPORT, это синонимы. Сделанные изменения не влияют на работоспособность класса в ехе-проекте, даже директива экспортируемого класса.

Далее в файл .срр проекта DLL нужно добавить функции создания и ликвидации объекта. Пример в [4] обходится без функции ликвидации, видимо, потому, что в приведенном там примере и DLL и импортирующее приложение написаны на Delphi, так что можно освободить память методом Free, который есть у всех наследников TObject и отсутствует у объектов С++, не имеющих общего класса-предка. Функция создания объекта должна просто вызывать конструктор, передать ему полученные от приложения параметры и вернуть указатель на созданный объект. Функция ликвидации принимает указатель на объект и вызывает деструктор. И обязятельно вписать эти функции в список экспортируемых.

И всё! Пятнадцать минут работы, при минимальном знании С++. Остальное в Delphi.

В импортирующей программе необходимо объявить класс, содержащий виртуальные открытые функции в том же порядке. Также необходимо объявить сложные структуры данных, используемые в DLL и передаваемые через ее границу в любом направлении. Имеются в виду структуры С++, они же записи Паскаля. И, конечно же, нужно объявить импортируемые функции создания и уничтожения класса. Теперь для создания экземпляра класса вызывается соответствующая функция DLL, когда объект перестает быть нужным - снова вызывается соответствующая функция DLL, а методы вызываются традиционно - "Объект.Метод(Параметры)". При этом обзывать методы в Delphi можно как угодно, важен лишь порядок их следования и списки параметров.

Если в С++ функция-член возвращает значение, в Delphi соответствующий метод должен быть тоже функцией. Если же функция-член возвращает void, в Delphi соответствующий метод - процедура.

Если в С++ параметр передается по значению, то и в Delphi тоже, если же по ссылке (то есть как указатель), то в Delphi такой параметр должен быть объявлен с ключевым словом var.

Чуть подробнее о параметрах и их типах. Практически везде, где говорится о DLL, упоминается, что, если хотите обеспечить совместимость DLL с программами на других языках, необходимо обеспечить совместимость типов. То есть, стремиться использовать стандартные типы ОС Windоws. Такие типы, как string или file вообще не совместимы с С++, с TDateTime можно поэкспериментировать, вообще-то, он соответствует стандарту, принятому в OLE-автоматизации ([3]). Опять же, [3] заявляет о соответствии типов single и double Delphi с float и double в С++ соответственно. Хотя в [5] есть такой совет со ссылкой на News Group: "Если вы создаете DLL не с помощью Delphi, то избегайте чисел с плавающей точкой в возвращаемом значении. Вместо этого используйте var-параметр (указатель или ссылочный параметр в С++) Причина кроется в том, что Borland и Microsoft применяют различные способы возврата чисел с плавающей точкой. Borland С++ и Delphi могут использовать один и тот же метод".

Таблица соответствия типов Delphi и С++:

Тип С++ Байтов Тип Delphi
int ?(4) integer
unsigned int ?(4) cardinal
char, __int8 1 shortint
short, __int16 2 smallint
long, __int32 (int) 4 longint (integer)
__int64 8 int64
unsigned char 1 byte
unsigned short 2 word
unsigned long 4 longword
float 4 single
double 8 double
char *   PChar