Техническа статия

Зареждане на нативната PDFium библиотека на всяка цел

PDFium компонентът намира своята нативна библиотека чрез фиксирана, подредена верига за търсене, вместо да го остави на loader-а на операционната система, защото deployment дърво, което е изрично, е deployment дърво, което можете да дебъгвате; На Windows тази верига търси поддиректория Win32 или Win64, която инсталаторът вече доставя; На други цели тя изгражда името на поддиректорията от Free Pascal target макросите, като <cpu>-<os>, така че deployment дървото се чете точно като дървото на компилираните модули; Това последно решение въведе дефект, който си заслужава цялата статия, защото причината беше главна буква, а симптомът беше мълчание

Веригата, по ред

Четири местоположения, опитвани в последователност, после platform loader-ът като последна инстанция; Първо предпочитаното оформление, директория DLLs до изпълнимия файл, съдържаща по една поддиректория за цел; Второ алтернативно оформление с target поддиректорията директно до изпълнимия файл; Трето плоското legacy оформление, библиотеката седяща до изпълнимия файл без изобщо поддиректория; Четвърто, само на Windows, системната директория, която се нуждае от грижа, защото 32-битов процес трябва да гледа в SysWOW64 и 64-битов процес в System32 и на 32-битов Windows първата не съществува, така че търсенето трябва да падне обратно; Едва след всичко това loader-ът бива помолен да търси сам

Диаграма на веригата за търсене на нативната PDFium библиотека за Delphi, от DLLs target поддиректорията през алтернативни, плоски и Windows системно-директорийни оформления до platform loader-а
Четири изрични местоположения се сондират по ред, преди OS loader-ът да бъде помолен да търси сам

Нарочно няма стъпка за системна директория извън Windows; Собственият път за търсене на platform loader-а, воден от конфигурацията на runtime linker и library path средата, вече покрива тази територия и дублирането му в Pascal би означавало преимплементиране на правила, които варират по дистрибуция; Диагностицирането на откази във Windows веригата е разгледано отделно в deploy на PDFium DLL и диагностициране на откази при зареждане

Откъде идва името на поддиректорията

На Windows то е Win32 или Win64, решавано от битовата ширина на изпълняващия се процес, а не на операционната система, защото това е това, което определя кой двоичен файл може да бъде зареден; Навсякъде другаде името е изградено от compiler target макросите, така че машина, компилираща за две архитектури, произвежда две ясно отделени дървета и така че папката, държаща нативната библиотека, седи до папката, държаща компилираните модули, със същото име

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // Компилаторните макроси пишат OS с главна буква („Linux“, „Darwin“),
  // докато изходната директория на package модула не, така че двете
  // съвпадат само след folding. На чувствителна към регистъра файлова
  // система тази разлика е цялото търсене
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Защо една главна буква счупи цялата верига

Компилаторното макро изписва целевата операционна система с начална главна буква: Win64, Linux, Darwin; Lazarus пакетът записва изхода на своите модули в директория, кръстена от собствена му target променлива, която е с малки букви: win64, linux, darwin; Две изписвания на едно и също и никакъв начин да се забележи на Windows, където файловата система не ги разграничава

На Linux те са две различни директории; Deployment, който слага shared object-а в DLLs/x86_64-linux, е невидим за loader, търсещ DLLs/x86_64-Linux, така че и четирите изрични стъпки на веригата пропускат и кодът пада през към оставянето на platform loader-а да търси; Понякога това работи, ако библиотеката случайно е инсталирана system-wide, и понякога не и и в двата случая внимателно подреденото deployment дърво не допринася нищо; Отказът няма съобщение за грешка, защото нищо не се е провалило: всяка стъпка правилно докладва, че файлът не е там, където тя гледаше

Как една главна буква в FPC target макрото чупи търсенето на PDFium DLL на Linux: loader-ът търси DLLs/x86_64-Linux, докато deployment папката е DLLs/x86_64-linux, което съвпада само на нечувствителен към регистъра Windows
Същата цел, изписана по два начина, съвпада на Windows и мълшиво пропуска на чувствителна към регистъра файлова система

Пробната програма, компилирана и изпълнена

Този клас дефекти не може да бъде намерен с четене и не може да бъде намерен и с компилиране; Обичайната техника за верифициране на platform клон, който никога не се компилира на машината за разработка, е да копирате модула във временна директория, да го преименувате, да замените platform условния израз със символ, който никога не е дефиниран, и да компилирате копието; ако се компилира, uses клаузата и call сигнатурите на този път са поне самосъгласувани; Това работи добре за самостоятелен модул

Тук не работи; Основният binding модул е много голям и дърпа LCL, така че не може просто да бъде копиран и компилиран с Windows символа изключен; Така че вместо това шепата функции, които промяната докосна, бяха преписани дословно в малка самостоятелна програма и тази програма беше пусната; Тя отпечата x86_64-Win64 и разминаването беше видимо в един ред изход; Компилирането на същата програма не би ви казало нищо, защото низът е напълно валиден; само стойността му е грешна

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // Печатайте, не assert-вайте. Точката е да погледнете стойността,
  // до която макро действително се разширява на тази toolchain
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

Общият урок: когато кръстосано-платформена промяна засяга стойността на нещо, а не неговия тип, само-компилаторна верификация не е верификация; Принтирайте го; По-широкият набор от кръстосано-компилаторни разлики между Delphi и Free Pascal е събран в статията за Delphi и FPC кръстосано-компилаторни капани

Оставете платформата да обясни собствените си откази при зареждане

Windows клонът на loader-а изброява на ръка причините, поради които зареждане може да се провали, защото полезните разграничения там — несъвпадение на архитектура, липсваща транзитивна зависимост, път, който не резолвира — съпоставят на error кодове, които си заслужават да бъдат назовани поотделно; Извън Windows портабилният loader модул вече връща описателен низ, който покрива същата територия, така че не-Windows клонът го използва директно, вместо да преизвежда категории от error число, което значи различни неща на различни системи

Съпротивата срещу порива да се нормализират тези две в едно съобщение е нарочна; Отказ при зареждане е deployment проблем и човекът, четящ съобщението, се нуждае от собствения речник на платформата, за да го потърси

Сблъсък на имена, който се рекурсира

Още един капан, малък и остър; Портабилният loader модул експортира процедура, казваща се UnloadLibrary, а binding модулът има процедура със същото име, която прави собственото си счетоводство, преди да пусне handle-а; Вътре в тази процедура неквалифицирано извикване на UnloadLibrary резолвира към онази в текущия модул, която вика себе си; Поправката е да се квалифицира извикването с името на модула

Това е същата форма като проблемите със засенчване на идентификатори, които доминират Free Pascal преносите изобщо: Windows модулът експортира целочислени типизирани функции за минимум и максимум, които засенчват floating-point такива и тип за синхронизация, който засенчва класа със същото име и във всеки случай резолюцията зависи от реда на uses клаузата; Квалифицирането на мястото на извикване е поправката, която не зависи от някой да запази този ред по-късно

Сблъсък на имена UnloadLibrary в PDFium компонента: неквалифицирано извикване в Delphi binding модула се рекурсира в себе си, докато модул-квалифицирано извикване достига портабилния loader модул и пуска handle-а
Квалифицирането на мястото на извикване изпраща пускането през loader модула, вместо да се рекурсира в binding модула

Deployment контролен списък

Три неща обясняват повечето откази при зареждане, щом пътната аритметика е права; Архитектурата трябва да съвпада с процеса, не с машината, така че 32-битово приложение на 64-битов Windows се нуждае от 32-битовия двоичен файл; V8-включената компилация има различно име на файл, така че deployment, който ги смесва, ще изглежда правилен и няма да зареди нищо; И само един вариант може да живее в системна директория в даден момент, което е добра причина да се предпочете изричното поддиректорийно оформление пред инсталирането на каквото и да е system-wide

За Lazarus конкретно, сложете нативната библиотека под DLLs/<cpu>-<os> с малки букви, до изпълнимия файл и тя ще бъде намерена от първата стъпка на веригата на всяка цел; Примерът viewer, който упражнява това на Lazarus, е описан в статията за Lazarus и FPC viewer, а текущата поддръжка на платформи е изброена на продуктовата страница PDFium Delphi component