Задачка №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.
Удалить