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

Библиотеки от тип DLL, ActiveX и dylib в PDF Library for Delphi: Извикване на PDF ядрото от всеки език за програмиране

Ето един проблем, който се появява в момента, в който една PDF библиотека напусне родния си език. Имате интеграция, която работи перфектно от C# в Windows. Нуждаете се от същите извиквания от Python в macOS, затова копирате файла с декларации от Windows, променяте името на двоичния файл и го стартирате. Всеки символ се разпознава успешно. Първото извикване обаче връща некоректни данни, второто се срива с грешка за нарушен достъп (access violation), а кодът ви за PDF не е променян. Проблемът е едно ниво под PDF: експортираните функции в Windows използват конвенцията Stdcall, докато dylib библиотеката за macOS експортира същите функции като Cdecl с водещо долно тире, и всяка външна декларация, която бърка тези детайли, поврежда стека още преди отварянето на първия документ

Целият този клас грешки произтича от едно дизайнерско решение, което е добре да разберете предварително. PDF Library for Delphi, PDF ядрото с достъпен изходен код на losLab за Delphi и C++Builder, обвива целия си обектен модел в един плосък фасаден клас TPDFlib, след което разпространява тази фасада в три двоични формата: Windows DLL с около 1250 експортирани функции, COM/ActiveX обект за автоматизация и dylib библиотека за macOS. Семантиката на PDF е идентична и в трите случая. Проблемите, с които можете да се сблъскате, се намират в ABI слоя отдолу: конвенции за извикване, кодиране на низове, управление на дескриптори и това коя страна има право да освобождава заделените буфери

Една фасада, три двоични формата

Всяка публична функция на TPDFlib има свой опростен еквивалент, започващ с DL пред името на метода. LoadFromFile става DLLoadFromFile, Encrypt става DLEncrypt, а NewSignProcessFromFile става DLNewSignProcessFromFile. Първият параметър на почти всяка експортирана функция е InstanceID, върнат от DLCreateLibrary, който замества референцията към обекта, която извикващият код в Delphi би използвал. Запомнете това съответствие отрано. То означава, че справочникът за API на Delphi служи и като документация за всеки друг език: всичко, което класът може да прави, е достъпно и през DLL под предвидимо име, като можете да четете сигнатурите на методите в Pascal, за да разберете как да направите извикването от Python или C#

Версията за Windows генерира PDFlibDLL32.dll и PDFlibDLL64.dll; изберете тази, която отговаря на разрядността на вашия хост процес, тъй като 64-битов Java или .NET процес не може да зареди 32-битова библиотека, независимо как е декларирана тя

Архитектурна диаграма на една фасада TPDFlib, изложена като Stdcall Windows DLL, Safecall ActiveX обект за автоматизация и Cdecl macOS dylib
И трите бинарни файла споделят една плоска PDF фасада, но се различават по конвенция на извикване, обработка на низове и изисквания за регистрация

Windows: Stdcall инстанции и двойки W/A функции

Всяка функция, приемаща текстови низове, съществува в два варианта. Версията с W приема PWideChar (UTF-16, естествен избор за .NET, Java и c_wchar_p в Python), а версията със суфикс А приема PAnsiChar. Двете имат идентична семантика и се различават само по кодирането, което прави смесването им трудно за проследяване: не се повдигат изключения, не се връщат кодове за грешка, а просто получавате повредени знаци в метаданните или несъществуваща грешка за липсващ файл при пътища със символи извън стандартния ASCII. Първата подобна грешка с кодирането обикновено отнема часове за отстраняване, тъй като симптомът сочи към данните, а причината е в декларацията

// Windows свързване (PDFlibDLL64.dll): Stdcall, обикновени имена на експорти
function DLCreateLibrary: Integer; stdcall;
  external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
  external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
  FileName, Password: PWideChar): Integer; stdcall;
  external 'PDFlibDLL64.dll' name 'DLLoadFromFile';

// macOS свързване: същата функция, Cdecl и префикс с долна черта при експорта
function DLCreateLibrary: Integer; cdecl;
  external 'PDFlibDylib.dylib' name '_DLCreateLibrary';

Изберете едно кодиране за съответната хост среда и го приложете в генератора на декларации. Практическо правило: ако езикът поддържа UTF-16 низове по подразбиране, използвайте само версиите с W и избягвайте фамилията А

macOS: същите имена, различна ABI спецификация

Библиотеката dylib експортира същия набор от функции с две систематични разлики: конвенцията за извикване е Cdecl вместо Stdcall и всяко име на експортирана функция започва с водещо долно тире (като _DLCreateLibrary, _DLLoadFromFile). И двете промени са механични, което ги прави идеални за автоматично генериране на декларации, но опасни при ръчно копиране на Windows декларациите. Поддържайте един основен списък с функции и генерирайте специфичните за всяка платформа декларации автоматично, ако инструментите ви позволяват това. В противен случай ще се сблъскате с повреждане на стека, което се възпроизвежда само на платформата, която тествате най-рядко

COM и ActiveX платформи: Safecall и Olevariant данни

За VB.NET, C#, VBScript и по-стари среди за автоматизация OCX версията обвива същата фасада в обект за автоматизация IDispatch, IPDFlibrary, като всеки метод е деклариран като Safecall. Тази конвенция променя начина на предаване на грешките. Safecall превежда вътрешните грешки в COM HRESULT стойности, така че извикващият код на C# хваща изключение там, където чистата DLL библиотека би върнала целочислен код за грешка, който трябва да се проверява ръчно. Една и съща операция води до два различни начина на обработка на грешки в зависимост от това кой двоичен файл сте заредили

Двоичните данни следват друго специфично за COM правило: интерфейсът за автоматизация не съдържа указатели като параметри. Всичко двоично - байтове на изображения при въвеждане или PDF байтове при изход - преминава през границата като тип Olevariant чрез методи като AddImageFromVariant and AppendToVariant. Преобразуването на масив от байтове във Variant се извършва с един ред в .NET. Опитът да се превърне в указател с аргумента, че се намира в същия процес, води до отхвърляне или повреждане на извикването от диспечерския слой. Още един детайл при регистрацията може да провали внедряването: COM регистрацията зависи от разрядността, така че OCX файл, регистриран с 32-битовата версия на regsvr32, е невидим за 64-битов хост процес. Това несъответствие се проявява като грешка "класът не е регистриран" на клиентската машина, дълго след като кодът е напуснал вашата работна среда

Дисциплина на дескрипторите: инстанциите притежават документите

Плоският API работи с целочислени дескриптори (handles). DLCreateLibrary връща инстанция на библиотеката. Зареждането на файл връща идентификатор на документ в рамките на тази инстанция. Процесите за подписване, списъците с низове и файловете с директен достъп връщат свои собствени дескриптори, като всички те са в обхвата на една и съща инстанция. Жизненият цикъл изглежда еднакво от всеки език чрез FFI, ето как се декларира в Pascal:

var
  Inst, Doc: Integer;
begin
  Inst := DLCreateLibrary;                       // един екземпляр за всяка работна нишка
  try
    Doc := DLLoadFromFile(Inst, 'in.pdf', '');   // returns a DocumentID, 0 on failure
    if Doc <> 0 then
    begin
      DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
        DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
      DLSaveToFile(Inst, 'out.pdf');
    end;
  finally
    DLReleaseLibrary(Inst);                      // освобождава всеки документ, притежаван от екземпляра
  end;
end;

От това дърво на собственост произтичат две неща. DLReleaseLibrary е единственото извикване за изчистване, от което действително се нуждаете, тъй като то освобождава всеки документ и дескриптор в рамките на инстанцията наведнъж. Това е достатъчно за кратки скриптове. При дълго работеща услуга обаче това води до изтичане на памет, така че освобождавайте документите веднага щом приключите с тях, вместо да ги оставяте да се натрупват до затварянето на инстанцията. Инстанцията е и естествената единица за изолация на нишки. Дайте на всяка работна нишка собствен InstanceID и никога не го споделяйте между нишки без външно синхронизиране, по същата причина, поради която не бихте споделили един обект TPDFlib

Върнатите низове са споделени, а не собствени

Функциите, които връщат текст, като DLGetPageText, предоставят указател PWideChar или PAnsiChar, който сочи към буфер, притежаван и преизползван от инстанцията на библиотеката. Договорът е следният: копирайте данните незабавно, никога не освобождавайте буфера ръчно

PDF Library for Delphi времева линия, противопоставяща копирането на заетия указател DLGetPageText веднага на задържането му до рециклирането на долния буфер от библиотеката
Върнатите char указатели заемат памет, която екземплярът рециклира, затова копирането трябва да стане преди следващото извикване на библиотеката
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // pointer into a library-owned buffer
  PageText := P;                 // копирайте сега; следващо извикване може да използва повторно буфера
end;

В C# това означава преобразуване на IntPtr в управляван низ преди следващото извикване на библиотеката. В ctypes на Python това означава извличане на низа от указателя веднага. Запазването на суровия указател между различните извиквания е сериозна грешка, която преминава успешно тестовете, но се проваля в реална среда при едновременни заявки, тъй като второто извикване презаписва буфера, който първото все още чете. Същото правило за собственост важи и за обратните извиквания (callbacks), регистрирани чрез DLSetProgressCallback. Всеки указател, който библиотеката предава на функцията за обратна връзка, е валиден само в рамките на тялото на функцията, като самият обект на обратната връзка трябва да остане жив в паметта (закрепен в среди с автоматично почистване на паметта), докато инстанцията може да го извиква. Унищожаването на делегата по средата на изпълнението е типична причина за неочаквани грешки за нарушен достъп в .NET среди

Вградете бърз тест (smoke test) в самата библиотека за интеграция и го изпълнете, преди да разпространите генерираните декларации. Изпробвайте по едно извикване от всяка категория, която обикновено разкрива грешки в ABI: функция без параметри като DLCreateLibrary за потвърждаване на конвенцията за извикване, функция с входящ низ, съдържащ символи извън ASCII диапазона за потвърждаване на кодирането, функция с изходящ низ за проверка на буфера и една умишлено неуспешна операция, за да проследите предаването на грешката към хоста. Това отнема петнадесет минути работа, но предотвратява проблеми с конвенциите и кодирането, които иначе биха довели до сривове при клиентите

PDF Library for Delphi мрежа два на два от сонди за димни тестове на свързването, обхващащи конвенция на извикване, кодиране на низове, заети буфери и проявяване на провали
Четири евтини проверки хващат проблеми с конвенцията, кодирането и собствеността, преди генерираните декларации изобщо да стигнат до машина на клиент

Конкретният случай с Python ctypes

ctypes в Python е интеграцията, която се разработва ръчно най-често, и е чудесен пример за илюстриране на различията между платформите. В Windows зареждате библиотеката с ctypes.WinDLL, за да приложите Stdcall конвенцията, свързвате W функциите без суфикс и декларирате текстовите параметри като c_wchar_p. В macOS използвате ctypes.CDLL за Cdecl конвенцията, като запазвате същия списък от функции и разрешавате имената им без водещото долно тире. Повечето FFI слоеве, включително ctypes, автоматично се справят с конвенцията за водещото долно тире в macOS, но е добре да потвърдите това с единичен тест, преди да започнете генерирането на стотици декларации

Има ясни отговори на два въпроса, свързани с внедряването. Стандартната DLL библиотека не изисква регистрация: regsvr32 се прилага само за ActiveX компонента, а DLL се разпространява чрез обикновено копиране, което е и основната причина да я предпочитате за Windows услуги и контейнери, където не искате да променяте регистъра. Безопасността при многонишково изпълнение се свежда до правилото: една инстанция на нишка. Дескрипторът на инстанцията съхранява всяко променливо състояние, което ядрото проследява: избрания документ, опциите за рендериране, настройките за извличане. Поради тази причина две нишки, споделящи една инстанция, ще си пречат взаимно, дори ако всяко отделно извикване връща успех

След като интеграцията е стабилна, операциите през нея съответстват на разгледаните в статиите за Delphi, включително прилагане и одит на PDF шифриране и извличане на текст и изображения от съществуващие документи

Двоичните файлове за трите нива на интеграция се разпространяват заедно с библиотеката; вижте продуктовата страница на PDF Library for Delphi за версии и лицензиране