Окей, я решил, наконец, написать какую-нибудь программку. Может быть Shareware. Может быть нет. Не знаю. Есть только пока несколько задумок.
Хочется сделать что-нибудь красивое в D2009 с применением всех тех новых возможностей Delphi.
Ну и, конечно же, как любая нормальная программка, моя софтина должна быть расширяемой за счёт плагинов. Не суть сейчас важно, плагины какого рода будут в ней - для старта нужна некая универсальная система, на базе которой можно легко собрать любые плагинчики, какие надо.
Для тех, кто мало знаком с темой плагинов, я быстренько пробежался по DK и нашёл вот такие статьи (сверху-вниз - от самого навороченного до голых DLL):
- COM-автоматизация. Не сказать, что совсем плагины, но что-то близкое.
- Плагины в виде COM-объектов.
- Плагины в виде пакетов + интерфейсы.
- Плагины в виде пакетов (bpl - packages).
- Плагины в виде DLL.
1. COM-объекты (2-я статья). По-первой прикидке вроде и неплохое решение, но уж как-то кажется оно мне тяжеловесным. Да и не силён я в COM, а тут же всякие нюансы нужно учитывать. И кроме того, мне не нравится идея отложенной выгрузки библиотеки, а не по требованию (да, я в курсе про проблемы явной выгрузки, но...). Посему: этот способ подождёт до лучших времён.
2. Голые DLL (5-я статья). Однозначно нет, т.к. недостатков здесь полно, а преимуществ нет вообще. К примеру, это проблемы с DLLMain. Обычное решение таких проблем выглядит так: не выполнять никакой работы в DLLMain (читай: в секциях initialization/finalization модулей, а также в begin/end dpr-файла), а выполнять её только по запросу. Первым действием после загрузки библиотеки плагина должен идти вызов функции инициализации, а первым действием перед выгрузкой - вызов функции освобождения. К сожалению, никакими средствами эту схему в Delphi для DLL вы не реализуете.
Помимо этой проблемы есть ещё проблема утомительного составления модулей импорта/экспорта, их корректной синхронизхации при модификации.
Но самое главное в плагинах в виде DLL - это контроль ошибок. Вы замучаетесь использовать здесь коды ошибок. Давно на голом WinAPI не писали? Будете делать плагины в виде чистых DLL - придётся вспомнить. А как же авторы плагинов, которые пишут в Delphi и слабо знакомы с WinAPI? Ну, обойдутся или придётся учиться.
3. Пакеты Delphi (4-я статья). В чистом виде - также однозначно нет. Потому что это явная привязка к Delphi. А плагины должны быть независимы от языка. Кроме того, здесь имеются серьёзные проблемы с сопровождением - при некоторых модификациях нужно полностью пересобирать все проекты.
4. ...и снова пакеты Delphi (нет статьи - можно считать улучшенным способом "голых DLL"). Дело в том, что пакет в Delphi - это просто обычная DLL, но с некоторыми достаточно интересными свойствами. Откинув те из них, что имеют отношение чисто к Delphi, мы увидим, что пакет - это DLL, у которой код инициализации/финализации в DLLMain сведён к минимуму, а весь реальный код повешен на две специальные функции: Initialize и Finalize. Каждую из этих функций можно вызвать после загрузки и до выгрузки, соответственно, таким образом, полностью избавляясь от проблем с DLLMain. Во, это то, что нам нужно. Если отбросить специфичную для Delphi шелуху, то пакет будет представлять собой DLL, в которой нет проблем с DLLMain. Заметим, что простое использование пакетов вовсе не привязывает нас к Delphi. Это будет зависеть от реализации (см. далее).
5. ...пакеты как DLL + интерфейсы ("почти" третья статья). Окей, в предыдущем пункте мы избавились от проблемы с DLLMain. Осталась самая малость: как реализовывать интерфейс между ядром и плагинами. Итак, у нас есть варианты:
- Функции. Хотя здесь нет проблем с межязыковой совместимостью, но всё же этот способ никуда не годится: фактически этим возвращаются все проблемы голых DLL, только что без проблемы с DLLMain.
- Объекты. Никуда не годится. Привязка к Delphi, со всеми вытекающими - как и при чистых пакетах.
- Интерфейсы. Во. Это уже что-то. Во-первых, здесь нет привязки к языку - интерфейсы доступны почти в любом языке под Win32 (а где их нет - то это исключение, подтверждающее правило; впрочем, я таких языков не знаю). Во-вторых, интерфейс позволяет работать по ООП со всеми преимуществами. Меньше и проблемы с импортом/экспортом. А самое главное, что интерфейсы позволяют (как и объекты) использовать для контроля ошибок исключения - через safecall-методы.
Для тех, кто вообще слабо знаком с интерфейсами, можно почитать Использование интерфейсов или Урок 4. Сервер, кокласс, интерфейс и Урок 5. Интерфейс IUnknown (с небольшим уклоном в COM). Вопрос производительности.
Вот это наш вариант!
Итак, для себя я получил, что идеальный вариант был бы плагины в виде пакетов, которые используются как простые DLL, а интерфейс ядро-плагины построен на базе интерфейсов. Что необходимо для того, чтобы эта система была бы независима от языка? Да всего-то самую малость: не передавать по интерфейсам никакой информации, специфичной для Delphi или конкретной версии компилятора - объекты (особенно Application и Screen), строки типа AnsiString/String/UnicodeString и т.п.
Причём, при желании, эта схема может быть (опционально и даже частями) легко дополнена до схемы "чисто пакеты + интерфейсы".
Например, главное приложение и входящие в базовую поставку плагины могут быть скомпилированы с использованием run-time пакетов Delphi (т.е. разделять между собой базовые пакеты Delphi - rtl, vcl и т.п.). При этом суммарный размер приложения уменьшается за счёт повторного использования кода, находящегося в стандартных пакетах (либо же можно собрать свой собственный пакет run-time поддержки).
Но при этом пакеты используются только как средство повторного использования кода - никаких иных бонусов с них получать нельзя (например, общий менеджер памяти). Самое главное, не использовать в пакетах языко-зависимых возможностей, а использовать их только как DLL - тогда и не будет межязыковых проблем.
Тогда сторонние плагины легко могут быть написаны как "пакеты в виде DLL + интерфейсы".
Как выглядят такие "пакеты в виде DLL" в других языках? По-большому счёту это будет просто DLL. Если язык позволяет собирать DLL без проблем с DLLMain - ну и супер, это идеально. Если же нет - ну что-ж, нужно просто аккуратнее писать плагин и не усугублять ситуацию (работают же как-то плагины в виде DLL с проблемами в DLLMain - и ничего).
По-большому счёту, даже в Delphi можно собрать плагин как DLL-проект, а не пакет - и встроить его в эту систему. Лишь бы интерфейс у DLL совпадал. И всё будет работать на отлично - если только вы не будете перегружать секции initialization/finalization кодом, а будете использовать специально введённые функции инициализации.
Читать далее.
Интересная статья.
ОтветитьУдалитьА ты не мог рассказать поподробнее о проблемах с контролем ошибок в DLL?
А разве нельзя в Delphi вообще ничего не писать в DllMain?
>>> А ты не мог рассказать поподробнее о проблемах с контролем ошибок в DLL?
ОтветитьУдалитьНа DK готовится к публикации моя статья по обработке ошибок. Я добавлю сюда ссылки после её выхода.
Вкратце я уже сказал: невозможность использование исключений - это зло. Это низкий уровень. Это чертовски неудобно.
А исключения - это только safecall. А safecall - это только объекты или интерфейсы.
Использование safecall позволяет писать плагин так, как если бы он не был внешней отдельной частью главного приложения, а был бы в него встроен. Т.е. не нужно специально оборачивать каждую функцию в оболочку для обработки ошибок и не нужно специально вписывать обработку ошибок после каждого вызова функции плагина.
>>> А разве нельзя в Delphi вообще ничего не писать в DllMain?
Нет, нельзя. Конкретно ты, как разработчик плагина можешь в своём коде вовсе не использовать секции initialization/finalization и begin/end в dpr-файле.
Но.
Стандартные модули уже используют initialization/finalization. Например, System - это как минимум менеджер памяти и поддержка исключений.
Иными словами, создать DLL с почти пустой DLLMain нельзя - там всегда будет код. Вот пакет - другое дело.
Конечно, ничего особо страшного в стандартном коде нет. Проблемы начнуться, когда в плагине потребуется использовать большую и сложную сторонюю библиотеку. В которой весьма сильно используются initialization/finalization - ну вот случилось так, что писали её в расчёте на использование в главном приложении (как вариант - в пакете), но не в DLL (там её даже не тестировали).
Конечно, можно плюнуть на все эти проблемы и сказать: всё - DLL. У кого с этим проблемы - это ваши проблемы. Но ведь хочется сделать красиво?
> Вкратце я уже сказал: невозможность использование исключений - это зло.
ОтветитьУдалитьКак это невозможность, можно с этого места поподробнее? Не понимаю. У меня, например, есть код: Dll и приложение, написанны на Дельфи. Приложение вызывает функцию из Dll, и если при выплнении этой функции в Dll возникает Exception, то в приложение этот exception замечательно ловится.
> Но ведь хочется сделать красиво?
В последнее время пришёл к выводу, что простой и понятный код, для меня важнее красивого но сложного. (blush)
p.s. Функции вызываются с директивой stdcall.
ОтветитьУдалитьp.p.s. Когда статья выйдет, запости пожалуйста. С удовольствием почитаю.
>>> то в приложение этот exception замечательно ловится
ОтветитьУдалитьДа ловится оно по той простой причине, что и приложение и библиотека написаны в Delphi!
Это легко сообразить самому.
Исключение в Delphi представляется объектом класса Exception (или наследником). При возбуждении исключения с ним ассоциируется объект. За удаление объекта ответственнен тот, кто ловит исключение.
Внимание, вопрос:
- Если приложение написано на C++ (MSVS), а исключение было возбуждено в DLL (Delphi), то как программа на C++ получит доступ к объекту Delphi? А как она его удалит?
- Как вы собираетесь в своей программе, написанной на Delphi, ловить исключения C++? Что вы знаете о C++ классах исключений? А как вы будете удалять объект C++?
- Если плагин написан в Delphi 2009, а главное приложение - в Delphi 2007, будет ли работать ваша схема? Как приложение получит доступ к данным UnicodeString?
- Как в общем случае (заранее не зная, на чём написан модуль - на C++, D2007 или D2009) вы определите правильный способ обхождения с исключением? А как вы узнаете, какой модуль возбудил исключение?
Бонус-вопрос:
- Почему исключения работают даже, если не используется ShareMem или его аналог? Как приложению удаётся удалять объект, созданный в чужом модуле, доступа к менеджеру которого у главного приложения нет. Подсказка: рассмотреть обязательную виртуальность деструктора.
Как видите, исключения можно безопасно использовать только в одном случае: если все модули используют одну и ту же версию RTL - т.е. ровно при тех же условиях, что и родные пакеты Delphi, со всеми вытекающими последствиями (привязка к версии и т.п.)...
>>> В последнее время пришёл к выводу, что простой и понятный код, для меня важнее красивого но сложного.
ОтветитьУдалитьА никто не заставляет использовать именно пакеты. Хотите DLL - пишите DLL. Они же взаимозаменяемые. DLL-плагин будет полностью работоспособен при этой схеме.
Я ровно об этом и говорю: пакет - это не более чем обычная DLL, с вынесенным отдельно кодом инициализации.
Суть в том, чтобы реализовать систему плагинов так, чтобы она допускала любой из этих двух стилей.
> Да ловится оно по той простой причине, что и приложение и библиотека написаны в Delphi!
ОтветитьУдалитьК моему стыду, до меня только сейчас дошло, что exception-ы это фича Дельфей, а не винды. Попытался вспомнить как Api сообщает об ошибках, смог вспомнить только результат вызова функции и GetLastError.
Интересные вопросы, я даже не задумывался о том, что как оно будет работать если собрать DLL с другой версией RTL.
Мой ответ на все эти вопросы: не знаю.
>>> Попытался вспомнить как Api сообщает об ошибках, смог вспомнить только результат вызова функции и GetLastError
ОтветитьУдалитьЕсли говорить очень грубо, то исключение в Windows - это запись типа TExceptionRecord (посмотрите в Windows.pas или в SysUtils.pas - другой, "более Дельфовый" вид). Оно характеризуется кодом (число) и некоторым количеством целочисленных параметров.
Код исключения - это либо один из стандартных кодов (например, EXCEPTION_ACCESS_VIOLATION - см. Windows.pas) или один из пользовательских кодов.
Библиотека поддержки каждого языка обычно включает в себя удобную оболочку вокруг системного механизма исключений. Для этого она резервирует под себя несколько пользовательских кодов исключений (например, Delphi - cDelphiException = $0EEDFADE и др. - см. System.pas). Объект языка, представляющий собой исключение, ассоциируется с системным исключением с помощью указания указателя на объект в одном из параметров исключения.
>>> Интересные вопросы, я даже не задумывался о том, что как оно будет работать если собрать DLL с другой версией RTL.
Если когда-нибудь использовали Total Commander с кучей плагинов, то могли видеть это в действии.
Total Commander написан на Delphi 2. Плагины к нему чаще всего пишутся на современных версиях Delphi. Проблема в том, что в D2 и, скажем, D7 используют разные коды исключений Delphi. Поэтому, если человек, писавший плагин к TC не был аккуратен и не заключал каждую свою функцию в try/except, то может быть такая ситуация, когда исключение убегает из плагина.
TC не может опознать это исключение как исключение Delphi (у них же разные коды), поэтому для него это - EExternalException. Информация об исключении сидит в ExceptionRecord - просто набор ничего не говорящих цифр.
Всё, что TC может сделать - это показать сообщение об ошибке (точный текст я не помню): "В приложении возникла ошибка EEDFADE. Продолжить выполнение программы?".
Далее - либо выход, либо продолжение работы с утечкой ресурсов (объект исключения-то никто не освобождал) и возможными проблемами в дальнейшем.
Кстати, поскольку TC написан на D2, то он не может использовать интерфейсы - они появились только в D3.
Пост обновлён: добавлены ссылки на материал про DllMain.
ОтветитьУдалить>>> А разве нельзя в Delphi вообще ничего не писать в DllMain?
ОтветитьУдалитьhttp://www.delphikingdom.ru/asp/answer.asp?IDAnswer=68224 - а вот и пример про DllMain.
Хм. Я за чистые dll тк не хочу ограничивать свои приложения только одной средой.
ОтветитьУдалитьПо поводу "утомительного составления модулей импорта/экспорта, их корректной синхронизхации при модификации", мне пришлась по душе идея оконной процедуры Windows, те dll редко имеет больше одной процедуры.
Пост обновлён: добавлены ссылки на статью
ОтветитьУдалитьhttp://www.delphikingdom.ru/asp/viewitem.asp?catalogid=1392
Добавил пожелание про нормальный DllMain для DLL на uservoice - можно голосовать ;)
ОтветитьУдалитьПроголосовал всем и оставшимися голосами.
ОтветитьУдалить> Первым действием после загрузки библиотеки
ОтветитьУдалить> плагина должен идти вызов функции
> инициализации, а первым действием перед
> выгрузкой - вызов функции освобождения. К
> сожалению, никакими средствами эту схему в
> Delphi для DLL вы не реализуете.
Реализуется очень просто и вполне легально - в begin-end определяется своя DLLMain, а в ней все делается через dwReason.
>>> Реализуется очень просто и вполне легально
ОтветитьУдалитьЧитайте внимательнее.
Секции initialization модулей выполняются (из DLLMain) до того, как ваш код получает управление.
> Примечание 12.01.2011: эта серия не была закончена.
ОтветитьУдалитьСерия будет завершена?
Таковых планов пока не имею.
ОтветитьУдалитьВечер добрый! А может вы можете создать специальный отдельный пост для random вопросов?
ОтветитьУдалитьВот хотел спросить совета, попытался найти подходящую по тематике тему, и как-то не вышло сходу.
Нам приходит некая переменная типа OleVariant. Не мы её создаём, и не мы её освобождаем. А в ней сидит некий объект, который поддерживает ряд потомков IUnknown.
Один из них условно назовём ISomeInterface. Но он может поддерживаться, а может и нет. Путём вытрясывания духа из Гугла удалось сварганить такое:
function SomeFunction(const Variable: OleVariant): Boolean;
var Ret: HRESULT; SomeInterface: ISomeInterface;
begin
Result:=False;
if Supports(Variable, IID_ISomeInterface, SomeInterface) then
begin
Ret:=SomeInterface.SomeMethod(1, 2, 3);
// SomeInterface._Release(); // не могу сообразить - надо ли здесь такую строчку?
if (Ret=S_OK) then
Result:=True
else
OleCheck(Ret);
end
else
raise Exception.Create('[Failed] Variable not supports IID_ISomeInterface');
end;
Подскажите пожалуйста, нужна там эта строчка или нет, всё ли в целом нормально, может какую ошибку где допустил... Спасибо!
Нет, _Release вызывать не нужно. Если охота - можно присвоить nil переменной SomeInterface.
УдалитьВ остальном нормально вроде (проверял на глаз).