Ето един проблем, който се появява в момента, в който една PDF библиотека напусне родния си език. Имате интеграция, която работи перфектно от C# в Windows. Нуждаете се от същите извиквания от Python в macOS, затова копирате файла с декларации от Windows, променяте името на двоичния файл и го стартирате. Всеки символ се разпознава успешно. Първото извикване обаче връща некоректни данни, второто се срива с грешка за нарушен достъп (access violation), а кодът ви за PDF не е променян. Проблемът е едно ниво под PDF: експортираните функции в Windows използват конвенцията Stdcall, докато dylib библиотеката за macOS експортира същите функции като Cdecl с водещо долно тире, и всяка външна декларация, която бърка тези детайли, поврежда стека още преди отварянето на първия документ
Целият този клас грешки произтича от едно дизайнерско решение, което е добре да разберете предварително. PDFlibPas, 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-битова библиотека, независимо как е декларирана тя
Windows: Stdcall инстанции и двойки W/A функции
Всяка функция, приемаща текстови низове, съществува в два варианта. Версията с W приема PWideChar (UTF-16, естествен избор за .NET, Java и c_wchar_p в Python), а версията със суфикс А приема PAnsiChar. Двете имат идентична семантика и се различават само по кодирането, което прави смесването им трудно за проследяване: не се повдигат изключения, не се връщат кодове за грешка, а просто получавате повредени знаци в метаданните или несъществуваща грешка за липсващ файл при пътища със символи извън стандартния ASCII. Първата подобна грешка с кодирането обикновено отнема часове за отстраняване, тъй като симптомът сочи към данните, а причината е в декларацията
// Windows binding (PDFlibDLL64.dll): Stdcall, plain export names
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 binding: same function, Cdecl, and an underscore prefix on the export
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; // one instance per worker thread
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); // frees every document the instance owns
end;
end;
От това дърво на собственост произтичат две неща. DLReleaseLibrary е единственото извикване за изчистване, от което действително се нуждаете, тъй като то освобождава всеки документ и дескриптор в рамките на инстанцията наведнъж. Това е достатъчно за кратки скриптове. При дълго работеща услуга обаче това води до изтичане на памет, така че освобождавайте документите веднага щом приключите с тях, вместо да ги оставяте да се натрупват до затварянето на инстанцията. Инстанцията е и естествената единица за изолация на нишки. Дайте на всяка работна нишка собствен InstanceID и никога не го споделяйте между нишки без външно синхронизиране, по същата причина, поради която не бихте споделили един обект TPDFlib
Върнатите низове са споделени, а не собствени
Функциите, които връщат текст, като DLGetPageText, предоставят указател PWideChar или PAnsiChar, който сочи към буфер, притежаван и преизползван от инстанцията на библиотеката. Договорът е следният: копирайте данните незабавно, никога не освобождавайте буфера ръчно
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copy now; a later call may reuse the buffer
end;
В C# това означава преобразуване на IntPtr в управляван низ преди следващото извикване на библиотеката. В ctypes на Python това означава извличане на низа от указателя веднага. Запазването на суровия указател между различните извиквания е сериозна грешка, която преминава успешно тестовете, но се проваля в реална среда при едновременни заявки, тъй като второто извикване презаписва буфера, който първото все още чете. Същото правило за собственост важи и за обратните извиквания (callbacks), регистрирани чрез DLSetProgressCallback. Всеки указател, който библиотеката предава на функцията за обратна връзка, е валиден само в рамките на тялото на функцията, като самият обект на обратната връзка трябва да остане жив в паметта (закрепен в среди с автоматично почистване на паметта), докато инстанцията може да го извиква. Унищожаването на делегата по средата на изпълнението е типична причина за неочаквани грешки за нарушен достъп в .NET среди
Вградете бърз тест (smoke test) в самата библиотека за интеграция и го изпълнете, преди да разпространите генерираните декларации. Изпробвайте по едно извикване от всяка категория, която обикновено разкрива грешки в ABI: функция без параметри като DLCreateLibrary за потвърждаване на конвенцията за извикване, функция с входящ низ, съдържащ символи извън ASCII диапазона за потвърждаване на кодирането, функция с изходящ низ за проверка на буфера и една умишлено неуспешна операция, за да проследите предаването на грешката към хоста. Това отнема петнадесет минути работа, но предотвратява проблеми с конвенциите и кодирането, които иначе биха довели до сривове при клиентите
Конкретният случай с 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 шифриране и извличане на текст и изображения от съществуващие документи
Двоичните файлове за трите нива на интеграция се разпространяват заедно с библиотеката; вижте продуктовата страница на PDFlibPas за версии и лицензиране