Компонент PDFium знаходить свою власну бібліотеку через фіксований, упорядкований ланцюг пошуку, а не віддає це завантажувачу операційної системи, бо дерево розгортання, що є явним, — дерево розгортання, яке ви можете налагоджувати. На Windows той ланцюг шукає підкаталог Win32 чи Win64, який уже постачає інсталятор. На інших цілях він будує назву підкаталогу з макросів цілі Free Pascal, як <cpu>-<os>, тож дерево розгортання читається точно як дерево скомпільованих модулів. Те останнє рішення внесло ваду, варту цілої статті, бо причиною була велика літера, а симптомом — тиша
Ланцюг, по порядку
Чотири місця, пробувані послідовно, а тоді завантажувач платформи як останній засіб. Перше — уподобане розташування, каталог DLLs поруч із виконуваним файлом, що містить один підкаталог на ціль. Друге — альтернативне розташування з цільовим підкаталогом прямо поруч із виконуваним файлом. Третє — пласке застаріле розташування, бібліотека сидить поруч із виконуваним файлом без усякого підкаталогу. Четверте, лише на Windows, — системний каталог, що потребує обережності, бо 32-бітовий процес мусить шукати в SysWOW64, а 64-бітовий — у System32, і на 32-бітовій Windows першого не існує, тож пошук мусить відкотитися. І лише після всього цього завантажувача просять шукати самостійно
Поза Windows кроку системного каталогу свідомо немає. Власний шлях пошуку завантажувача платформи, керований конфігурацією компонувальника виконання та середовищем бібліотечних шляхів, уже покриває ту землю, і дублювання його в Pascal означало б повторну реалізацію правил, що різняться за дистрибутивами. Діагностику невдач у ланцюзі Windows охоплено окремо в розгортанні DLL PDFium і діагностиці невдач завантаження
Звідки береться назва підкаталогу
На 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