Delphi and DLL

S

SsEH

Использование в делфи DLL
И вопросы касательно этой темы

Сначала немного теории о использовании
Для понимания как механизма возникновения проблем при использовании DLL, так и методики их решения, необходимо разобраться в работе линковщика.
Линковка - процесс, собирающий из разрозненного объектного кода2 итоговый исполняемый файл. Необходимость линковки вызвана желанием строить программу из отдельных фрагментов, откомпилированных в разное время и часто - разными людьми. С точки зрения нашей темы линковка важна прежде всего тем, что именно здесь к программе подключаются стандартные библиотеки Delphi, в частности, библиотека компонент (VCL).
Обычный, действующий по умолчанию механизм иногда называют статической линковкой. Исходные файлы Delphi компилируются в объектный код - файлы с расширением dcu (Delphi Compiled Unit). После этого линковщик собирает из dcu итоговый файл; из каждого объектного файла берется только тот код, те подпрограммы, которые прямо или косвенно используются в программе3.
В первых версиях Delphi стандартная библиотека, равно как и добавленные пользователем компоненты, хранились в одном большом файле с расширением dcl (Delphi Compiled Library). Фактически в этом файле хранились dcu-шки модулей, составляющих библиотеку; они линковались в программу тем же способом. Чуть позже DCL разбили на ряд меньших по размерам пакетов и стали использовать расширение dcp; принципы статической линковки остались теми же самыми.
Особый момент в логике линковщика - обработка DLL, динамически подключаемых библиотек. Если объектный код декларирует использование подпрограммы из DLL, линковщик создает для нее заглушку - псевдоподпрограмму, состоящую по сути из оператора безусловного перехода (jmp). Загрузчик Windows, готовя программу к выполнению, подгружает DLL и инициализирует таблицу переходов реальными адресами подпрограмм; таким образом, код, обращающийся к функции DLL, вызывает как подпрограмму команду jmp и управление передается реальной подпрограмме из DLL4.
Вместе с реализацией пакетов появилась и альтернативная схема линковки библиотек. В результате компиляции пакета, кроме dcp-файла создается также файл с расширением bpl (Borland Package Library). По сути этот файл представляет собой DLL, экспортирующую все интерфейсные функции входящих в нее модулей, а также ряд дополнительных механизмов, добавленных Borland. Эти библиотеки - называемые run-time packages - подключаются примерно так же, как DLL. В результате исполняемый файл получается намного меньшим по размеру, но вместе с ним приходится распространять несколько bpl-файлов. Иногда этот подход называют динамической линковкой. Именно этот вариант сборки - использование run-time packages - позволяет построить корректно работающее приложение.
Для ликвидации терминологической путаницы в этой статье используется следующая терминология:
Статически линкуемым (модулем или пакетом) будем называть объектный код (dcu или dcp), включенный линкером в соответствующий исполняемого файла.
Динамически линкуемым (пакетом, DLL) будем называть объектный код, откомпилированный в виде отдельного исполняемого файла и подключаемый к основному механизмом статической либо динамической загрузки.
Статически загружаемыми будем называть DLL (в том числе и пакеты), автоматически загружаемые при старте приложения описанным выше образом.
Динамически загружаемыми будем называть DLL, явно загружаемые и выгружаемые приложением (например, в варианте LoadLibrary/GetProcAddress).

В результате статической линковки пакетов в каждый выполняемый файл копируются - линкуются - одни и те же библиотеки, в результате чего каждый exe и каждая dll поддерживают свой экземпляр общих, по идее, данных - глобальных переменных, экземпляров синглтонов и так далее. "Общим модулем" же оказываются в том числе ключевые файлы стандартной библиотеки: менеджер памяти, стандартные классы и компоненты, и подобные - не приспособленные для работы в таком режиме. В частности, предыдущий пример показывает неожиданное дублирование информации RTTI - "сердца" всей VCL. Если не предпринять специальных мер, формы, созданные в DLL, не попадут в в список Screen.Forms; компонент TQuery, созданный в DLL, не найдет компонент TDatabase основной программы и так далее. Последствия же этого сказываются в работе других частей стандартного кода - например, форма, созданная в DLL, оказывается в таскбаре, поскольку не находит главного окна приложения. В результате получается огромный запутанный клубок проблем и ошибок.
Также проблемами при статической линковке пакетов обусловлена популярная легенда "в DLL нельзя передавать и из DLL нельзя возвращать объекты, динамические массивы, строки типа AnsiString и так далее". На самом деле - можно и нужно; в DLL можно делать все, что можно делать в обычном exe-файле. Но - при использовании динамической линковки пакетов, при использовании run-time packages.
Для библиотек, собранных с использованием run-time packages, ни одной подобной проблемы не возникает; для них просто нет возможности. Общие модули загружаются в виде bpl - и хотя пакет может использоваться во многих местах, в памяти приложения он присутствует в единственном экземпляре, как код, так и данные. Конфликты и расхождения при этом исключены. Довольно часто программисты пытаются использовать статическую линковку пакетов в DLL чтобы работать "как привычно", "как с единственным exe-шником", "без лишних сложностей, в которых пока не хочется разбираться". На деле же как раз динамическая линковка обеспечивает абсолютно привычную работу без каких-либо особенностей, в то время как для работы приложения со статической линковкой приходится предпринимать множество усилий с заведомо сомнительным результатом.

Собственно, на использовании run-time packages можно было бы закончить статью - это простой, надежный и безошибочный режим. В то же время удивительно много людей предпочитает набивать себе шишки - а потому необходимо упомянуть о возможных путях неправильного решения проблемы, средствах организации работы без run-time packages.
Прежде всего, следует попытаться синхронизировать глобальные переменные и объекты. В частности, переменные Application и Screen в DLL следует инициализировать значениями, переданными из главной программы - это указатели, а следовательно, можно направить несколько указателей на один и тот же физический объект в памяти. В ряде случаев можно опереться на хандлы используемых объектов; в частности, в BDE-приложениях можно использовать в каждой DLL собственный компонент TDatabase, инициализированный одинаковым с другими значением Database.Handle.
Ряд возможностей VCL - например, конструктор CreateParented - предназначен для организации взаимодействия с приложениями, созданными в других средах. С их помощью возможно четко разделить "зоны ответственности" - в частности, избежать ошибок при сочетании на форме объектов, созданных в различных модулях. Некоторые функции VCL - например, FindControl - опираются на возможности Windows и нормально работают при статической линковке пакетов.
Одним из модулей, входящих в стандартную библиотеку, является менеджер динамической памяти. Он используется не только при явных операциях с динамической памятью, но также при создании объектов, при работе со строками и динамическими массивами. При статической линковке пакетов у каждого исполняемого файла оказывается собственный менеджер памяти - и, в первую очередь, собственная копия его внутренних структур. Как результат, блок памяти, выделенный в одном из модулей, не может быть корректно обработан - изменен или освобожден - в другом модуле; менеджер памяти второго модуля просто не найдет в своих данных упоминания об этом блоке памяти и выдаст ошибку "Invalid pointer operation". Именно поэтому передача в/из DLL данных сложных типов - строк, объектов - требует дополнительных усилий.
В составе Delphi поставляется модуль ShareMem, позволяющий решить эту проблему. Для нормальной работы этот модуль должен быть указан первым в списке uses каждого из проектов, составляющих приложение - только так можно гарантировать, что все операции с динамической памятью будут выполнены с использованием правильного менеджера памяти.
Модуль ShareMem достаточно прост. Он использует возможность установки внешнего менеджера памяти, общего для всех DLL, вместо используемого по умолчанию. В нем предлагаются два варианта: могут использоваться либо стандартные функции Windows (GlobalAlloc/GlobalFree), либо специальный менеджер памяти (библиотека borlndmm.dll). Первый вариант плох с точки зрения производительности; второй требует распространения вместе с проектом еще одной, дополнительной dll; тем не менее, оба варианта решают ряд проблем. Для однородного проекта лучшим было бы третье решение, которого, к сожалению, в стандартной поставке нет - взять менеджер памяти exe-шника и использовать его же во всех подгружаемых dll. В любом случае, таким образом устанавливается единый для всех dll менеджер памяти.
Необходимо отметить, что модуль ShareMem решает далеко не все проблемы статической линковки - скажем, приведенный выше пример демонстрирует глюк несмотря на наличие ShareMem в списке uses.
При использовании run-time packages модуль ShareMem не вносит заведомых ошибок, но бессмысленен и вреден - приложение и так использует общий менеджер памяти, любые данные передаются между модулями без каких-либо дополнительных усилий, и нет необходимости подгружать дополнительный и менее эффективный модуль. Также стоит помнить, что в этом случае нельзя быть "немножко беременной" - если модуль ShareMem используется хоть где-нибудь, он должен использоваться везде и быть указан первым во всех списках uses. В противном случае часть переменных окажется обработанной старым менеджером памяти, часть - новым, и это практически наверняка приведет к крайне сложным в отладке ошибкам.

Вывод
Для Delphi существует технология, позволяющая разрабатывать и использовать DLL практически
без каких-либо дополнительных усилий и практически без каких-либо дополнительных проблем.
Существенные факторы, мещающие ее использовать, мне не известны. В то же время отступление
от этой технологии почти гарантирует более или менее серьезные неудобства в случае разработки
приложений, состоящих из нескольких выполняемых файлов (exe и dll). Существующие приложения
могут быть довольно легко приведены к правильной технологии и это практически всегда ликвидирует
возникшие проблемы; однако, конвертация не является полностью прозрачной, тривиальной задачей1.
Единственный вариант, когда использование пакетов заведомо не решит всех проблем - неоднородная
среда разработки, например, разработка на Delphi DLL к недельфовому проекту. Но даже в этом случае
описываемая технология уменьшит количество возникающих проблем.

А теперь сам вопрос. Судя по статье если использовать RUNTIME линковку то все будет ОК.
Но судя по всему проблемы на этом не заканчиваютсяа только начинаются.
Проблема Runtime 216 Access Violation проблемы с выгрузкой из памяти приложения и модулей.
Так вот у меня при использовании компонентов DevExpress в часности тех которые связаны каким-то образом
с uxTheme.dll (TdxBarManager, TdxLayoutControl и т.д.).
Вот так и живем, в инете вобщем-то море таких ошибок проплавает. Уже была така ошибка только она была специфична для компонентов
JEDI прежних версий.
Может у кого есть мысли на этот счет.
А может у кого просто есть хороший совет чтобы поделится по теме.
 
S

SsEH

Расследование такого поведения компонентов DevExpress продолжается.
Первое что удалсь обнаружить путем детального иследования Exception violation это то что компоненты DevExpress содержат в себе библиотеку XP Theme Manager которая работает с хендлами контролов и окон, так вот судя по всему если использовать в dll компонент и он при закрытии приложения раньше выгружается чем модуль dxThemeD10.bpl то вот вам и Runtime 216 Access Violation проблема может кто знает в как этому помочь!!!

Решение найдено
Нужно в модуде MainFotm в finalize вызвать функцию CloseAllThemes из пакета DevExpress и все будет а ажуре. Удачи всем.
Почемуто детального описания правил последовательности загрузки и выгрузки модулей при RUNTIME компиляции еще ни разу не встречал.
 
Последнее редактирование модератором: