Задачка №19 была подготовкой к этой статье. Похоже, что я плохо сформулировал задачку и/или дал мало подсказок. Хотя в ней имеется целых две проблемы.
Давайте разберём проблему номер один, ради которой всё и затевалось, а в конце статьи в P.S. я дам описание второй проблемы.
Итак, предполагалось, что ответ смогли бы назвать многие, кто хоть раз писал эксперт/компонент для среды Delphi или разрабатывал плагины для своей программы.
Проблема
Дело в том, что типичный BPL-пакет для IDE (а BPL - это тоже DLL) как правило является "design-time only", т.е. предназначен для использования только внутри самой IDE. Т.е. этот пакет содержит в себе только регистрацию компонента в IDE (ну и/или эксперт). А чтобы программируемые вами приложения можно было собирать с пакетами, делают ещё "run-time only" пакет, который содержит собственно код компонента. Т.е. вы имеете два пакета: run-time и design-time. В первом лежит компонент, который программа грузит и использует, во втором лежит регистрация компонента в IDE и, возможно, дополнительные плюшки (в виде IDE экспертов).Ну или если вы разрабатывали более-менее сложную систему плагинов, то наверняка:
- Складывали все плагины в отдельную папочку, скажем подпапку
\Plugins
, чтобы не засорять папку приложения; - Выносили подпапку
\Plugins
изProgram Files
вAppData
, чтобы иметь возможность устанавливать/удалять плагины без необходимости взятия прав администратора; - Плагины наверняка использовали и другие DLL (например, движок БД, imaging-библиотеку или API к чему-либо), которые нужно было класть вместе с DLL плагинов.
В обоих случаях вы столкнётесь с проблемой загрузки ваших DLL с помощью кода из задачки №19 (загрузки design-time пакета IDE, которая использует похожий код, либо загрузки DLL плагина вашим ядром):
Что происходит? Ведь в условиях задачки явно сказано, что DLL по указанному пути есть и она читается.
Дело в том, что обработка ошибок
LoadLibrary
основана на кодах ошибок. Это означает, что функция при неудаче возвращает код ошибки - некое число. Если эта функция вызывает другие функции, и какая-либо из вызываемых функций заканчивается неудачей, то LoadLibrary
передаст её код вызывающему (вам). Таким образом, у вас нет возможности узнать, кому принадлежит возвращаемый код ошибки - непосредственно ли LoadLibrary
или какой-либо из вызываемых ей функций.Иными словами, ошибка "модуль не найден" ссылается не на
Project2.dll
. А на кого? Дело в том, что
Project2.dll
обязательно импортирует некоторые функции из других DLL. Делать это любая библиотека может двумя способами - либо динамически (через GetProcAddress
), либо статически (через external
). Если с GetProcAddress
всё более-менее понятно (вы указываете полный путь к библиотеке для импорта), то что насчёт статического связывания? Где системный загрузчик будет искать импортируемые библиотеки?MSDN подсказывает, что, во-первых, статический импорт через
external
из библиотеки с именем 'name.dll'
эквивалентен загрузке библиотеки только по имени (т.е. LoadLibrary('name.dll')
), во-вторых, по умолчанию система ищет DLL в следующих папках (и в указанном порядке):- Уже загруженные DLL;
- Список "известных" DLL;
- Папка приложения (т.е. папка, в которой лежит .exe);
- Текущая папка;
- Системная папка (т.е. System32);
- Папка Windows;
- Папки, указанные в переменной окружения
PATH
.
Обратите внимание, что в этом списке нет "папки, в которой лежит загружаемая DLL".
Иными словами, если вы загружаете DLL
Project2.dll
из подпапки \DLLs
вашего приложения, а Project2.dll
статически импортирует функцию из некой другой SomeOther.dll
(например, DLL встроенной базы данных или SSL библиотеки) из той же подпапки \DLLs
, то система не сможет найти SomeOther.dll
при загрузке Project2.dll
- что и приведёт к показу сообщения об ошибке "Не найден указанный модуль", т.е. не найден SomeOther.dll
."Наивные" варианты решения
Как решить эту проблему? Очевидно, что вариантов - тьма:- Вместо статического связывания через
external
использовать динамическое связывание черезGetProcAddress
с указанием полного абсолютного пути вLoadLibrary
; - Предварительно загрузить все статические связи DLL;
- Внести статически связанные DLL в список "известных" DLL;
- Вынести статически связанные DLL в папку приложения;
- Сменить текущую папку на подпапку
\DLLs
перед загрузкой; - Хранить статические связанные DLL в папке System32;
- Хранить статические связанные DLL в папке Windows;
- Включить подпапку
\DLLs
в переменную окруженияPATH
; - Использовать средства Windows для разрешения конфликтов DLL: DLL Redirection (Windows 2000+) или сборки/манифесты (Windows XP+);
- Кое-что ещё ;)
Project2.dll
, а о всех библиотеках, которые Project2.dll
статически импортирует через external
- т.е. о SomeOther.dll
)Разумеется, варианты эти не равноценны. Давайте посмотрим, что и когда стоит применять, а что применять вообще не нужно.
Использовать GetProcAddress
вместо external
Вариант первый (замена external
на GetProcAddress
) - оптимален, но не всегда возможен (в частности, если вы используете КДЛ - "Код Других Людей"). Также, это не будет ответ на задачку, поскольку в задачке речь идёт именно про проблемную загрузку DLL со статическим связыванием.Предварительно загрузить все статические связи DLL
Вариант второй - реален, но на практике, пожалуй, не применим. Ведь, по сути, чтобы успешно его использовать, вам нужно знать, какие DLL статически импортирует загружаемая вамиProject2.dll
, и явно загрузить каждую из них. Пожалуй, единственный случай, когда этот сценарий вообще применим - если вам нужно загрузить все DLL из указанной папки.Внести статически связанные DLL в список "известных" DLL
Вариант третий используют "важные" системные DLL, которые присутствуют в единственном экземпляре. Список "известных" DLL хранится по адресуHKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs
:Как вы можете видеть, этот список предназначен для системных DLL, чтобы при загрузке
LoadLibrary('kernel32.dll')
у вас всегда загружалась бы C:\Windows\System32\kernel32.dll
, а не C:\Temp\Hacker\kernel32.dll
. Кроме того, система кэширует эти библиотеки для ускоренного доступа. Таким образом, этот вариант не следует использовать сторонним разработчикам (и вам в том числе).Вынести статически связанные DLL в папку приложения
Вариант четыре тоже достаточно простой и его можно использовать, когда нет возможности использоватьGetProcAddress
. Но в условии задачки явно сказано, то мы хотим вынести DLL в подпапку \DLLs
, так что этот вариант не является ответом.Сменить текущую папку на подпапку \DLLs
перед загрузкой
Вариант пять - первый из рабочих:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; OldDir: String; // <- добавлено begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; OldDir := GetCurrentDirectory; // <- добавлено try // <- добавлено SetCurrentDirectory(ExtractFilePath(DLLFileName)); // <- добавлено DLL := LoadLibrary(PChar(DLLFileName)); finally // <- добавлено SetCurrentDirectory(OldDir); // <- добавлено end; // <- добавлено Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;Да, этот код успешно загрузит
Project2.dll
и все её статические зависимости (при условии, что библиотеки лежат в той же подпапке \DLLs
). Но это решение имеет два недостатка:
- Никогда не используйте относительные пути - это, во-первых, не потокобезопасно. Более того, поскольку внутри вызова
LoadLibrary
выполняетсяDLLMain
всех загружаемых DLL (что в Delphi равнозначно выполнению всех секцийinitialization
всех модулей всех DLL), то это также не безопасно и в рамках одного потока; - Кроме того, это решение использует всего одну папку, нельзя загружать DLL из разных папок (например, помимо
\DLLs
использовать\Plugins
)
Хранить статические связанные DLL в папке System32
Хранить статические связанные DLL в папке Windows
Варианты шесть и семь, как несложно сообразить, предназначены в первую очередь - для системных DLL, во вторую - для "общих компонентов" ("компонентов" - не в смысле Delphi). Современные рекомендации Microsoft состоят в том, что сегодня место на диске - дёшево, поэтому приложениям следует по возможности хранить частные копии DLL у себя в папках, а не расшаривать их через папку System32 - дабы уменьшить вероятность DLL Hell. Поэтому это решение (тупо скидывать DLL в System32, чтобы избавиться от ошибки) - я бы сказал, "на двоечку". И да, это не решение задачи, где мы говорим про библиотеки в подпапке \DLLs
.Включить подпапку \DLLs
в переменную окружения PATH
Вариант восемь также работает:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; OldPATH: String; // <- добавлено PATH: String; // <- добавлено begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; OldPATH := GetEnvironmentVariable('PATH'); // <- добавлено try // <- добавлено PATH := ExtractFilePath(DLLFileName) + ';' + OldPATH; // <- добавлено SetEnvironmentVariable('PATH', PChar(PATH)); // <- добавлено DLL := LoadLibrary(PChar(DLLFileName)); finally // <- добавлено SetEnvironmentVariable('PATH', PChar(OldPATH)); // <- добавлено end; // <- добавлено Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;Заметьте, что нам нет нужды менять системные переменные окружения, мы можем изменить только свою локальную копию. Также заметьте, что мы не меняем никаких настроек в реестре, мы просто динамически меняем блок переменных нашего процесса.
Что ж, по сравнению с
SetCurrentDirectory
, у этого решения есть два больших плюса:
- Мы можем указать несколько папок;
- Откуда следует, что это решение, хотя и не потокобезопасно, но значительно лучше в плане повторной входимости, поскольку последующий код не затрёт нашу папку, а добавит к ней.
PATH
уже содержится слишком много путей, то попытка добавить к нему ещё одну папку провалится. Если честно, я не знаю, завершится ли вызов SetEnvironmentVariable
неудачей или же добавит обрезанную переменную. В любом случае, это не самый вероятный сценарий на практике, так что это решение - "на четыре".А вот решение, которое использует сама среда Delphi (статически включать папки в
PATH
, прописывая их в профиле пользователя) - это явно не выше "двойки". Несколько установленных IDE, кучка разных компонентов, плюс сторонние программы - вот место в PATH
и закончилось. Неоднократно сталкивался с этим, плююсь каждый раз. Причём у проблемы нормального решения нет. Понятно, что можно почистить PATH
от мусора, но это временное решение. А так - либо переустановка софта в папки с более короткими именами, либо создание ссылок на папки. Как вариант, если в PATH
много однотипных путей, то можно вынести общую часть путей в отдельные переменные окружения.Использовать средства Windows для разрешения конфликтов DLL
Вариант девять (вернее, их два - перенаправление и сборки) рассмотрен в отдельной статье, но это не будет ответом на задачку, т.к. на код загружающего оба варианта не повлияют.Что нам остаётся ещё?
Решение
Заметим, что можно изменить сам порядок поиска DLL.Во-первых, можно включить "безопасный" режим поиска - по сути, это тот же стандартный список, только он просто выносит вперёд системные каталоги и выносит текущую папку в конец. Смысл телодвижений в сужении площади атаки для хакеров. Если хакеру откроется доступ к текущей папке, то хакер сможет положить в текущую папку какую-нибудьДалее, у нас есть расширенная версияsecurity.dll
(да, есть такая системная DLL). Так что если потом ваше приложение загружаетsecurity.dll
(или любую другую DLL, которая статически связана сsecurity.dll
), то система загрузит DLL хакера, а не системную - посколькуsecurity.dll
отсутствует в списке "известных" DLL. В "безопасном" режиме система сначала проверит System32, а текущую - в конце, так что будет загружена системнаяsecurity.dll
.
Если вам кажется, что получить доступ к текущей папке не так-то просто, то это - заблуждение. Более того, если вы "оппортунистски" пытаетесь загрузить (несуществующую) DLL, то это будет вектором атаки даже с "безопасным" порядком поиска DLL.
В любом случае, "безопасный" порядок включен по умолчанию в новых ОС, выключен в старых, меняется в реестре (HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\SafeDllSearchMode
) и не вносит в список новые пути, он просто переупорядочивает уже имеющиеся.
LoadLibrary
- LoadLibraryEx
, которая дополнительно принимает некоторые флаги, меняющие поведение.Подсказка:
LoadLibrary
реализована так:function LoadLibrary(lpLibFileName: PChar): HMODULE; stdcall; begin Result := LoadLibraryEx(lpLibFileName, 0, 0); end;
В частности, в
LoadLibraryEx
есть флаг, специально предназначенный для нашего случая: LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
- этот флаг заменяет пути поиска DLL на "папку DLL" (и только её). А если вы хотите использовать и другие пути поиска, то можете добавить флаги LOAD_LIBRARY_SEARCH_APPLICATION_DIR
(для папки приложения), LOAD_LIBRARY_SEARCH_SYSTEM32
(для системной папки) и LOAD_LIBRARY_SEARCH_USER_DIRS
(для поиска по "пользовательским" папкам - см. ниже). Комбинацию флагов LOAD_LIBRARY_SEARCH_APPLICATION_DIR
, LOAD_LIBRARY_SEARCH_SYSTEM32
и LOAD_LIBRARY_SEARCH_USER_DIRS
можно заменить флагом LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
, но LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
туда не входит. Разумеется, чтобы флаг LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
работал, имя DLL, передаваемое в LoadLibraryEx
должно быть полным и абсолютным.Заметьте, что эти флаги полностью заменяют список путей поиска, поэтому при указании этих флагов в путях поиска не будут присутствовать текущий каталог и папки из
PATH
. Также заметьте, что эти флаги доступны только начиная с Windows Vista/Windows 7 с установленным KB2533623 и/или KB2758857 (эти обновления не включены в последние сервис-паки для Vista и 7, поэтому должны быть установлены отдельно - что обычно выполняется автоматически Windows Update). На Windows 8 и выше особых требований нет. Как узнать, что нужные обновления стоят и можно использовать этот метод? Ну, указанное обновление добавляет в систему, например, функцию
AddDllDirectory
, поэтому:function IsKB2533623Installed: Boolean; begin Result := Assigned(GetProcAddress(GetModuleHandle(kernel32), 'AddDllDirectory')); end;
Таким образом, наш код становится таким:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; DLL := LoadLibraryEx(PChar(DLLFileName), 0, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR or LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- изменено Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;А если вам лень каждый раз вызывать
LoadLibraryEx
вместе со всеми флагами, то вы можете вызвать SetDefaultDllDirectories
, указав набор флагов - и все дальнейшие вызовы LoadLibraryEx
без флагов (а, следовательно, и вызовы LoadLibrary
) будут автоматически использовать указанный порядок. Например:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR or LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- добавлено DLL := LoadLibrary(PChar(DLLFileName)); Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;Почти аналогичного эффекта можно добиться с использованием флага
LOAD_WITH_ALTERED_SEARCH_PATH
- этот флаг добавляет "папку DLL" к стандартному списку поиска DLL (а не заменяет его) и ставит её на третье место (после уже загруженных DLL и после "известных DLL") в этом списке (в обоих вариантах - стандартном и "безопасном"). Разумеется, флаг LOAD_WITH_ALTERED_SEARCH_PATH
взаимно исключает флаги LOAD_LIBRARY_SEARCH_APPLICATION_DIR
, LOAD_LIBRARY_SEARCH_SYSTEM32
, LOAD_LIBRARY_SEARCH_USER_DIRS
, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
и LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
(и, следовательно, LOAD_WITH_ALTERED_SEARCH_PATH
нельзя передать в SetDefaultDllDirectories
). Получаем:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; DLL := LoadLibraryEx(PChar(DLLFileName), 0, LOAD_WITH_ALTERED_SEARCH_PATH); // <- изменено Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;Второй вариант предпочтительнее по соображениям совместимости, поскольку также включает в себя все обычные места поиска, включая текущий каталог и пути из
PATH
, в то время как первый вариант их не включает. Зато первый вариант более защищён (безопасен) и позволяет установить умолчания, что позволяет не менять по коду вызовы LoadLibrary
.Оба решения полностью безопасны (в плане многопоточности и повторной входимости), но позволяют использовать только специально выбранные папки, а не произвольные. Существенным минусом также является работоспособность только на Windows Vista и Windows 7, обновлённых до уровня 12 июля 2011 года, и на Windows 8 и выше. Если бы не ограничение на версию системы, то это было бы решение на "пятёрочку", а так - "четыре с минусом".
Но это ещё не все варианты. Начиная с Windows XP SP 1 в системе есть функция
SetDllDirectory
. Она работает аналогично флагу LOAD_WITH_ALTERED_SEARCH_PATH
- добавляя произвольную папку в стандартные пути поиска DLL, но с четырьмя отличиями:
- Папка, добавляемая к путям поиска, указывается явно - в параметре функции (т.е. может быть любой, а не только "папкой DLL");
- Папка, добавляемая к путям поиска, вставляется на четвёртое место (после уже загруженных DLL, после "известных DLL" и после папки приложения), а не на третье;
- Текущий каталог исключается из путей поиска;
- Настройки "безопасного" списка игнорируются, поиск всегда производится по списку, эквивалентному списку "безопасного" поиска, за исключением двух отличий, указанных в п1 и п3 этого списка (добавили папку и удалили текущий каталог).
SetDllDirectory
позволяет указать только одну папку. Последующий вызов просто заменит предыдущую папку. Чтобы удалить папку и вернуть стандартные пути поиска - передавайте в функцию nil
.Т.е. в некотором смысле
SetDllDirectory
просто заменяет текущий каталог в списке путей поиска на определённую папку, не трогая сам текущий каталог.Итого:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; SetDllDirectory(PChar(ExtractFilePath(DLLFileName))); // <- добавлено try // <- добавлено DLL := LoadLibrary(PChar(DLLFileName)); finally // <- добавлено SetDllDirectory(nil); // <- добавлено end; // <- добавлено Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;Что-ж, у этого варианта лучше поддержка среди систем, также в плюсе - возможность указывать произвольную папку, но существенным минусом является использование глобального состояния для решения локальной проблемы. Итого - только "четыре".
"Улучшенный" вариант
SetDllDirectory
появляется в системе всё в том же KB2533623 - это функция AddDllDirectory
. Аналогично SetDllDirectory
, AddDllDirectory
добавит указанную папку в пути поиска DLL, только:
- Можно указать не одну, а несколько папок;
- Папки, переданные в
AddDllDirectory
по умолчанию не применяются к вызовамLoadLibrary
иLoadLibraryEx
без параметров: вам нужно или использоватьLoadLibraryEx
с флагомLOAD_LIBRARY_SEARCH_USER_DIRS
(илиLOAD_LIBRARY_SEARCH_DEFAULT_DIRS
) или вызыватьSetDefaultDllDirectories
с этими же флагами;
AddDllDirectory
вставляются в список поиска после "папки DLL" (если указан LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
) и после папки приложения (если указан LOAD_LIBRARY_SEARCH_APPLICATION_DIR
), но до системной папки (если указан LOAD_LIBRARY_SEARCH_SYSTEM32
).Функция
AddDllDirectory
возвращает cookie, которое можно передать в RemoveDllDirectory
для удаления папки из путей поиска. Эта особенность делает это решение безопасным и в плане многопоточности и в плане повторной входимости.Код:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; AddDllDirectory(PChar(ExtractFilePath(DLLFileName))); // <- добавлено DLL := LoadLibraryEx(PChar(DLLFileName), 0, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- изменено Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;или:
procedure TForm1.Button1Click(Sender: TObject); type TImportedProc = procedure; safecall; var DLLFileName: String; DLL: HMODULE; Test: TImportedProc; begin DLLFileName := ExtractFilePath(ParamStr(0)) + 'DLLs' + PathDelim + 'Project2.dll'; SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); // <- добавлено AddDllDirectory(PChar(ExtractFilePath(DLLFileName))); // <- добавлено DLL := LoadLibrary(PChar(DLLFileName)); Win32Check(DLL <> 0); try try Test := GetProcAddress(DLL, 'Test'); Win32Check(Assigned(Test)); Test; except Application.HandleException(Sender); end; finally Test := nil; FreeLibrary(DLL); DLL := 0; end; end;
Конечно, для практического применения я бы рекомендовал также добавлять флаг
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
в оба примера. В целом применять
AddDllDirectory
не имеет большого смысла если вам нужно добавить в пути поиска только "папку DLL", поскольку полностью аналогичный эффект достигается либо LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR
, либо LOAD_WITH_ALTERED_SEARCH_PATH
. AddDllDirectory
имеет смысл применять только если вам нужно добавить несколько папок, либо если нужно добавить одну папку, но она не является "папкой DLL".Вот, собственно, и все наши варианты. Всего их получилось четырнадцать штук (если не считать список "известных DLL")! Из них на практике я бы порекомендовал использовать несколько вариантов в комплексе:
- Если
IsKB2533623Installed
вернулаTrue
, то используйтеLoadLibraryEx
с указанием полного пути к DLL и флагомLOAD_WITH_ALTERED_SEARCH_PATH
(как альтернативный вариант: вызовите в начале программыSetDefaultDllDirectories
и далее используйте как обычноLoadLibrary
- этот вариант даже более актуален, если вы используете пакеты и стандартнуюLoadPackage
, где у вас нет возможности заменить вызовLoadLibrary
наLoadLibraryEx
с флагами внутриLoadPackage
); - В противном случае - проверьте наличие
SetDllDirectory
и "заворачивайте" в неё каждый вызовLoadLibrary
(и/илиLoadPackage
); - Если нет и
SetDllDirectory
, то используйте либо решение с локальным изменениемPATH
, либоSetCurrentDirectory
, либо и то и то.
- Если
IsKB2533623Installed
вернулаTrue
, то используйтеAddDllDirectory
; - В противном случае - используйте решение с локальным изменением
PATH
.
P.S. Да, а что это за вторая проблема задачки, которую я упомянул в начале статьи?
Хм, мне было странно, что эту проблему никто не назвал, ведь она довольно известна - настолько, что в Delphi есть специальная функция для обхода этой проблемы. Дело в том, что в строчке
DLL := LoadLibrary(PChar(DLLFileName))
будет выполнена DllMain(DLL_PROCESS_ATTACH)
указанной DLL - что приведёт к инициализации всех её модулей (вызове секций initialization
). Т.е. будет выполняться пользовательский код DLL. Этот пользовательский код DLL может быть не слишком вежлив и поменять глобальное состояние процесса, а именно - управляющее слово FPU (CWR). Что-ж, если загружаемая DLL написана на Delphi и она ссылается только на системные DLL и на другие DLL, тоже написанные на Delphi, то эта проблема не столь актуальна, т.к. управляющее слово будет заменено на то же самое значение, что и установлено (и ожидается) вашей программой. Но если или сама DLL или любая DLL, с которой наша DLL статически связана, будет написана на чём-либо ещё - то у такой DLL может быть своё представление о "правильном" управляющем слове. После этого дальнейшая работа вашей программы может стать непредсказуемой, даже если вы лично не используете типы с плавающей точкой. Например, простейший
Move
в новых версиях Delphi реализован через MMX команды процессора, которые являются расширением математического сопроцессора. Что, собственно, может приводить к тому, что тривиальный вызов Move
с заведомо корректными аргументами будет вылетать.Чтобы обойти эту проблему, нужно восстановить управляющее слово FPU на "ваше" значение. Чтобы не делать это вручную, в Delphi есть специальная функция -
SafeLoadLibrary
.Замечу только, что
LoadPackage
использует внутри себя именно SafeLoadLibrary
, а 64-битный код вообще не восстанавливает управляющее слово FPU, поскольку Delphi не использует математический сопроцессор в 64-битном коде (вместо этого используется SSE).Так что - да, не используйте
LoadLibrary
. Используйте SafeLoadLibraryEx
(написание SafeLoadLibraryEx
остаётся вам в качестве домашнего упражнения).Читать далее: Разработка API (контракта) для своей DLL.
Решение со сменой текущей папки не такое уж и плохое. Особенно если используются плагины сторонних разработчиков, которые любят делать TIniFile.Create('Settings.ini') прямо в секции initialization.
ОтветитьУдалитьНу, текущая папка - это средство, которое стоит выполнять дополнительно к основному методу - "на всякий случай".
Удалитьну хоть на троечку... спасибо )
ОтветитьУдалитьЯ поступал другим способом. Ставил хук на LdrLoadDll (ntdll). И к строковому параметру путей добавлял свой путь. Вот только надо быть осторожным, функция в Win10 ведёт немного себя иначе, чем в других Windows.
ОтветитьУдалитьНа пять с плюсом, чего уж там :)
УдалитьПодскажите, какой модуль надо подключить, что бы в Delphi XE10 стала доступной функция AddDllDirectory ?
ОтветитьУдалитьили вот так ?
Удалитьfunction AddDllDirectory(lpPathName: LPCWSTR): Integer; stdcall; external kernel32 name 'AddDllDirectory';
Delphi не слишком активно обновляет свои заголовочные файлы (читай: почти не обновляет вообще). Поэтому большинство современных функций в ней просто нет (где "современных" = "времён Windows XP"). Поэтому можно использовать JEDI API.
УдалитьИли, в данном случае, проще объявить ручками.
Большое спасибо!
Удалитьза ответ и вообще за прекрасный блог, он очень многому меня научил
Undeclared identifier: 'LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR' Что надо подключить чтобы видело это ?
ОтветитьУдалитьЭто решается поиском по .pas файлам.
УдалитьОтвет на подобные вопросы всегда такой:
1. Использовать последнюю версию Delphi со свежими заголовочниками (в данном конкретном случае - этой константы нет даже в Delphi 10.3).
2. Использовать сторонние заголовочники (например, JEDI WinAPI - но там её тоже нет).
3. Объявить самостоятельно. В частности:
const
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = $100;
Зря ты это конечно не указал сразу, я еле нашёл
Удалитьconst
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = $00000100;
LOAD_LIBRARY_SEARCH_DEFAULT_DIRS = $00001000;
Доброго времени суток. Загружаю из Delphi 10.2 (Windows 10 x64) dll, написанную на C# внутри с формой на WPF. Испробовал все перечисленные способы, но зависимые dll она видит только возле exe файла, загружающего библиотеку. Текущая директория у dll всегда папка exe, как бы я не пытался её перед запуском поменять. Флаги тоже результата не дают. Может быть Вы сталкивались с подобной ситуацией. DLL тоже наша но разобраться не можем. Зависимости грузятся не при загрузке DLL, а при инициализации расположенной в ней формы. LoadLibrary отрабатывает нормально, но при вызове метода создающего форму падает внутреннее исключение в dll. Если положить рядом с exe файлами все зависимые dll то всё работает. Расположение самой загружаемой DLL роли н играет, гружу по полному пути. Не подгружаются MaterialDesignColors.dll, MaterialDesignThemes.Wpf.dll и bos_games.Commons.dll.
ОтветитьУдалитьЯ вообще не разбираюсь в C#, но подозреваю, что у вас не просто DLL, а сборка. У сборок есть манифест, декларирующий зависимости и способы разрешения.
УдалитьПопробуйте использовать sxstrace.
Подробнее: https://www.gunsmoker.ru/2011/02/dll-dll-hell-dll-side-by-side.html
Посмотрите https://docs.microsoft.com/ru-ru/dotnet/standard/assembly/resolve-loads
УдалитьВот рабочий пример: https://github.com/achechulin/loodsman/blob/master/Plugins/PluginSampleNet/PluginFunctions.cs#L151
Всем спасибо за помощь! Помогло AssemblyResolve.
Удалить