Компонент PDFium находит свою нативную библиотеку через фиксированную упорядоченную цепочку поиска, а не оставляет это системному загрузчику ОС, потому что явное дерево развёртывания — дерево, которое можно отлаживать. На Windows эта цепочка ищет подкаталог Win32 или Win64, который уже поставляет установщик. На других целях она строит имя подкаталога из макросов цели Free Pascal, как <cpu>-<os>, чтобы дерево развёртывания читалось в точности как дерево скомпилированных модулей. Последнее решение внесло ошибку, стоящую всей статьи, потому что причиной была заглавная буква, а симптомом — тишина
Цепочка, по порядку
Четыре места, проверяемые по очереди, затем загрузчик платформы как последнее средство. Первое — предпочитаемая раскладка, каталог DLLs рядом с исполняемым файлом, содержащий по подкаталогу на цель. Второе — альтернативная раскладка с подкаталогом цели прямо рядом с исполняемым файлом. Третье — плоская унаследованная раскладка, библиотека рядом с исполняемым файлом вовсе без подкаталога. Четвёртое, только на Windows, системный каталог, требующий аккуратности, потому что 32-битный процесс должен искать в SysWOW64, а 64-битный — в System32, а на 32-битной Windows первого не существует, и поиску приходится откатываться. Лишь после всего этого загрузчика просят искать самостоятельно
Шага с системным каталогом вне Windows сознательно нет. Собственный путь поиска загрузчика платформы, управляемый конфигурацией компоновщика времени выполнения и переменными окружения путей библиотек, уже покрывает эту территорию, и дублирование его в Паскале означало бы пере-реализацию правил, различающихся между дистрибутивами. Диагностика сбоев в цепочке Windows отдельно разобрана в материале о развёртывании PDFium DLL и диагностике сбоев загрузки
Откуда берётся имя подкаталога
На Windows это Win32 или Win64, что решается разрядностью работающего процесса, а не операционной системы, ведь именно она определяет, какой двоичный файл можно загрузить. Везде иначе имя строится из макросов цели компилятора, чтобы машина, собирающая для двух архитектур, давала два чётко разделённых дерева и чтобы папка с нативной библиотекой лежала рядом с папкой скомпилированных модулей под тем же именем
function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
if IsWin64 then
Result := 'Win64'
else
Result := 'Win32';
{$ELSE}
// Макросы компилятора пишут ОС с заглавной ("Linux", "Darwin"), а
// выходной каталог модулей пакета — нет, поэтому оба совпадают лишь
// после приведения регистра. На файловой системе, чувствительной
// к регистру, это различие — весь поиск
Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;
Почему заглавная буква сломала всю цепочку
Макрос компилятора пишет целевую операционную систему с заглавной буквы: Win64, Linux, Darwin. Пакет Lazarus пишет вывод модулей в каталог, названный по собственной переменной цели, которая в нижнем регистре: win64, linux, darwin. Два написания одного и того же, и на Windows заметить это невозможно, где файловая система их не различает
На Linux это два разных каталога. Развёртывание, положившее разделяемую библиотеку в DLLs/x86_64-linux, невидимо для загрузчика, ищущего DLLs/x86_64-Linux, поэтому все четыре явных шага цепочки промахиваются, и код проваливается к тому, чтобы платформенный загрузчик искал сам. Иногда это работает, если библиотека случайно установлена общесистемно, иногда нет, и в любом случае аккуратно устроенное дерево развёртывания не даёт ничего. У отказа нет сообщения об ошибке, потому что ничего не отказало: каждый шаг корректно сообщил, что файла там, где он искал, не было
Пробная программа: скомпилированная и запущенная
Этот класс ошибок нельзя найти чтением, и компиляцией тоже. Обычный приём проверки платформенной ветви, которая никогда не компилируется на машине разработки, — скопировать модуль во временный каталог, переименовать его, заменить платформенную условную компиляцию символом, который никогда не определён, и скомпилировать копию; если она компилируется, раздел uses и сигнатуры вызовов на этом пути хотя бы самосогласованы. Для самодостаточного модуля это хорошо работает
Здесь это не работает. Главный связывающий модуль очень велик и тянет LCL, поэтому его нельзя просто скопировать и скомпилировать с выключенным символом Windows. Вместо этого горстка функций, которых коснулось изменение, была дословно переписана в маленькую самодостаточную программу, и эта программа была запущена. Она напечатала x86_64-Win64, и несовпадение стало видно в одной строке вывода. Компиляция той же программы не сказала бы ничего, потому что строка совершенно валидна; неверно только её значение
program ProbeSubDir;
{$MODE DELPHI}
uses
SysUtils;
begin
// Печатайте, а не проверяйте утверждением. Суть — увидеть значение,
// в которое макрос реально разворачивается на этом тулчейне
Writeln('raw: ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
{$I %FPCTARGETOS%}));
end.
Общий урок: когда кроссплатформенное изменение касается значения чего-либо, а не его типа, проверка одной компиляцией — не проверка. Напечатайте его. Более широкий набор межкомпиляторных различий между Delphi и Free Pascal собран в статье о межкомпиляторных ловушках Delphi и FPC
Позвольте платформе объяснять собственные сбои загрузки
Ветвь загрузчика под Windows вручную перечисляет причины, по которым загрузка может сорваться, потому что полезные там различия — несовпадение архитектуры, отсутствующая транзитивная зависимость, путь, который не разрешается, — отображаются в коды ошибок, достойные отдельных имён. Вне Windows переносимый модуль загрузчика уже возвращает описательную строку, покрывающую ту же территорию, поэтому ветвь для не-Windows использует её напрямую, а не выводит категории заново из номера ошибки, который на разных системах значит разное
Сопротивление желанию нормализовать эти два механизма в одно сообщение — сознательное. Сбой загрузки — проблема развёртывания, и человеку, читающему сообщение, нужен собственный словарь платформы, чтобы её искать
Коллизия имён, которая уходит в рекурсию
Ещё одна ловушка, маленькая и острая. Переносимый модуль загрузчика экспортирует процедуру UnloadLibrary, и у связывающего модуля есть процедура с тем же именем, которая ведёт собственный учёт перед освобождением дескриптора. Внутри той процедуры неквалифицированный вызов UnloadLibrary разрешается в одноимённую в текущем модуле, которая вызывает саму себя. Исправление — квалифицировать вызов именем модуля
Это тот же рисунок, что у проблем затенения идентификаторов, доминирующих в портах Free Pascal вообще: модуль Windows экспортирует целочисленные функции минимума и максимума, затеняющие вещественные, и тип синхронизации, затеняющий одноимённый класс, и в каждом случае разрешение зависит от порядка раздела uses. Квалификация места вызова — исправление, не зависящее от того, сохранит ли кто-то этот порядок позже
Контрольный список развёртывания
Три вещи объясняют большинство сбоев загрузки, когда арифметика путей верна. Архитектура должна совпадать с процессом, а не с машиной, поэтому 32-битному приложению на 64-битной Windows нужен 32-битный двоичный файл. Сборка с V8 имеет другое имя файла, поэтому развёртывание, смешивающее их, будет выглядеть правильно и не загрузит ничего. И только один вариант может жить в системном каталоге единовременно, что является хорошим поводом предпочесть явную раскладку подкаталогов установке чего-либо общесистемно
Для Lazarus конкретно положите нативную библиотеку в DLLs/<cpu>-<os> в нижнем регистре, рядом с исполняемым файлом, и её найдёт первый шаг цепочки на каждой цели. Пример программы просмотра, упражняющий это на Lazarus, описан в статье о просмотрщике Lazarus и FPC, а текущая поддержка платформ перечислена на странице продукта PDFium Delphi component