مقاله فنی

اتصال DLL، ActiveX و dylib کتابخانه PDF در دلفی

مشکلی هست که درست همان لحظه‌ای ظاهر می‌شود که یک کتابخانه PDF از زبان مادری خود بیرون می‌رود. اتصالی (binding) دارید که از C# روی ویندوز بی‌نقص کار می‌کند. همان فراخوانی‌ها را از Python روی macOS لازم دارید، پس فایل اعلان ویندوز را کپی می‌کنید، نام باینری را عوض می‌کنید و اجرا می‌کنید. همه نمادها حل می‌شوند. اولین فراخوانی مقدار بی‌معنی برمی‌گرداند، دومی با یک نقض دسترسی (access violation) از کار می‌افتد، و هیچ بخشی از کد PDF شما تغییر نکرده است. عیب یک لایه پایین‌تر از PDF است: صادرات‌های ویندوز از قرارداد Stdcall استفاده می‌کنند، dylib مربوط به macOS همان توابع را به‌صورت Cdecl و با یک زیرخط پیشوندی صادر می‌کند، و اعلان تابع خارجی‌ای که هر یک از این دو جزئیات را اشتباه بگیرد، پیش از باز شدن حتی یک سند، پشته را خراب می‌کند

کل این دسته از خرابی‌ها از یک تصمیم طراحی ناشی می‌شود که ارزش دارد از همان ابتدا آن را بشناسید. PDF Library for Delphi، موتور PDF با کد منبع در دسترسِ losLab برای Delphi و C++Builder، کل مدل شیء خود را در یک کلاس نمای (facade) مسطح واحد به نام TPDFlib می‌پیچد و سپس آن نما را در سه شکل باینری عرضه می‌کند: یک DLL ویندوز با تقریباً ۱٬۲۵۰ تابع صادرشده، یک شیء اتوماسیون COM/ActiveX و یک dylib برای macOS. معناشناسی PDF در هر سه یکسان است. بخشی که شما را گاز می‌گیرد در ABI زیرین زندگی می‌کند: قراردادهای فراخوانی، کدگذاری رشته‌ها، مالکیت هندل‌ها و این‌که کدام طرف مجاز است کدام بافر را آزاد کند

یک نما، سه شکل باینری

هر تابع عمومی TPDFlib یک همتای مسطح دارد که نامش DL به‌علاوه نام متد است. LoadFromFile به DLLoadFromFile تبدیل می‌شود، Encrypt به DLEncrypt و NewSignProcessFromFile به DLNewSignProcessFromFile. اولین پارامتر تقریباً هر صادراتی یک InstanceID است که DLCreateLibrary برمی‌گرداند و جای ارجاع شیئی را می‌گیرد که یک فراخواننده دلفی در غیر این صورت نگه می‌داشت. این نگاشت را زود در ذهن جا بیندازید. معنایش این است که مرجع API دلفی هم‌زمان مستندات هر زبان دیگری هم هست: هر کاری که کلاس بتواند بکند، DLL می‌تواند زیر نامی قابل‌پیش‌بینی انجام دهد، و می‌توانید یک امضای متد Pascal را بخوانید تا فراخوانی موردنیازتان را از Python یا C# یاد بگیرید

ساخت ویندوز PDFlibDLL32.dll و PDFlibDLL64.dll را تولید می‌کند؛ آن را انتخاب کنید که با بیت‌بودگی فرایند میزبان شما مطابقت دارد، زیرا یک فرایند ۶۴ بیتی Java یا .NET نمی‌تواند کتابخانه ۳۲ بیتی را بارگذاری کند، اعلان هر شکلی هم که باشد

دیاگرام معماری یک نمای TPDFlib که به‌صورت DLL ویندوز با Stdcall، شیء اتوماسیون ActiveX با Safecall و dylib مربوط به macOS با Cdecl عرضه شده است
هر سه باینری یک نمای مسطح PDF مشترک دارند اما در قرارداد فراخوانی، مدیریت رشته و الزامات ثبت با هم تفاوت دارند

ویندوز: نمونه‌های Stdcall و جفت‌توابع W/A

هر صادراتی که رشته می‌گیرد دو بار وجود دارد. یک نسخه wide که PWideChar می‌گیرد (UTF-16، گزینه طبیعی برای .NET، Java و c_wchar_p در Python)، و یک نسخه با پسوند A که PAnsiChar می‌گیرد. این دو معناشناسی یکسانی دارند و فقط در کدگذاری متفاوت‌اند، و دقیقاً همین است که ردیابی قاطی‌کردن آن‌ها را چنین دردناک می‌کند: هیچ چیزی استثنا نمی‌اندازد، هیچ چیزی کد خطا برنمی‌گرداند، صرفاً در متادیتا حروف درهم‌ریخته (mojibake) می‌گیرید یا برای هر مسیری که نویسه‌ای فراتر از ASCII ساده داشته باشد، یک «file not found» بی‌پایه. اولین باگ کدگذاری‌ای که یک تیم به این شکل به آن برمی‌خورد معمولاً یک بعدازظهر هزینه دارد، چون علامت به داده اشاره می‌کند و علت در اعلان است

// اتصال ویندوز (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 را متصل کنید و دیگر هرگز به خانواده A دست نزنید

macOS: همان نام‌ها، ABI متفاوت

dylib همان مجموعه توابع DL را با دو تغییر نظام‌مند صادر می‌کند. قرارداد فراخوانی به‌جای Stdcall، Cdecl است، و هر نام صادراتی یک زیرخط پیشوندی دارد (_DLCreateLibrary، _DLLoadFromFile و به همین ترتیب). هر دو تغییر کاملاً مکانیکی هستند، که آن‌ها را برای یک اتصال تولیدشده ایده‌آل و برای یک کپی دستی‌ویرایش‌شده از فایل ویندوز خطرناک می‌کند. اگر ابزارتان اجازه می‌دهد، یک فهرست تابع استاندارد واحد نگه دارید و اعلان‌های هر پلتفرم را از روی آن تولید کنید. این کار را نکنید و دقیقاً همان خرابی پشته‌ای را می‌گیرید که در بالای این صفحه شرح داده شد، آن هم فقط روی پلتفرمی که CI شما کمتر از همه آن را تمرین می‌دهد

میزبان‌های COM و ActiveX: Safecall و بارهای Olevariant

برای VB.NET، C#، VBScript و میزبان‌های اتوماسیون قدیمی، ساخت OCX همان نما را در یک شیء اتوماسیون IDispatch به نام IPDFlibrary می‌پیچد که هر متد آن Safecall اعلان شده است. این قرارداد شیوه رسیدن خطاها به شما را تغییر می‌دهد. Safecall یک خرابی داخلی را به یک HRESULT از COM ترجمه می‌کند، پس یک فراخواننده C# جایی استثنا می‌گیرد که DLL مسطح یک عدد صحیح خاموش برمی‌گرداند که فراخواننده باید یادش می‌ماند آن را بررسی کند. یک عملیات، دو اصطلاح خرابی، بسته به این‌که کدام باینری را بارگذاری کرده‌اید

داده باینری از قاعده دوم و ویژه COM پیروی می‌کند. رابط اتوماسیون اصلاً هیچ پارامتر اشاره‌گری ندارد. هر چیز باینری، بایت‌های تصویری که وارد می‌شوند یا بایت‌های PDF که بیرون می‌آیند، به‌صورت یک Olevariant از طریق متدهایی مانند AddImageFromVariant و AppendToVariant از مرز عبور می‌کند. مارشال‌کردن یک آرایه بایت به یک variant در .NET یک خط است. اگر با این استدلال که به‌هرحال همان فرایند است، سعی کنید به‌جایش یک اشاره‌گر خام بدهید، لایه dispatch فراخوانی را رد می‌کند یا خرابش می‌کند. یک جزئیات ثبت دیگر هم استقرارها را زمین می‌زند: ثبت COM به ازای هر بیت‌بودگی جداگانه است، پس OCXی که با regsvr32 سی‌ودو بیتی ثبت شده برای یک میزبان ۶۴ بیتی نامرئی است. این ناسازگاری به‌صورت پیام معروفِ بی‌فایده «class not registered» روی دستگاه مشتری ظاهر می‌شود، مدت‌ها پس از آن‌که دستگاه شما را ترک کرده است

انضباط هندل: نمونه‌ها مالک سندها هستند

API مسطح روی هندل‌های عدد صحیح کار می‌کند. DLCreateLibrary یک نمونه برمی‌گرداند. بارگذاری یک فایل یک شناسه سند درون آن نمونه برمی‌گرداند. فرایندهای امضا، فهرست‌های رشته و فایل‌های دسترسی مستقیم هر کدام هندل عدد صحیح خودشان را برمی‌گردانند، همگی در محدوده همان نمونه. چرخه حیات از هر میزبان FFI به یک شکل به نظر می‌رسد، که اینجا به Pascal نشان داده شده چون خواناتر است:

var
  Inst, Doc: Integer;
begin
  Inst := DLCreateLibrary;                       // یک نمونه به ازای هر رشته کارگر
  try
    Doc := DLLoadFromFile(Inst, 'in.pdf', '');   // یک DocumentID برمی‌گرداند، در صورت شکست 0
    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 تنها فراخوانی پاک‌سازی‌ای است که اکیداً به آن نیاز دارید، چون هر سند و هندل فرایند زیر نمونه را یک‌جا از بین می‌برد. در یک اسکریپت کوتاه همین کافی است. در یک سرویس طولانی‌مدت تبدیل به یک نشت آهسته با تشریفات اضافی می‌شود، پس سندها را همین‌که کارتان با آن‌ها تمام شد آزاد کنید، نه این‌که بگذارید تا مرگ نمونه روی هم انباشته شوند. نمونه همچنین واحد طبیعی ایزوله‌سازی رشته‌ها (threads) است. به هر رشته کارگر InstanceID خودش را بدهید و هرگز یکی را بدون قفل خارجی بین رشته‌ها به اشتراک نگذارید، به همان دلیلی که هرگز یک شیء TPDFlib واحد را بین رشته‌ها به اشتراک نمی‌گذارید

رشته‌های برگردانده‌شده قرضی هستند، نه متعلق به شما

توابعی که متن برمی‌گردانند، مانند DLGetPageText، یک PWideChar یا PAnsiChar تحویل می‌دهند که به بافری اشاره می‌کند که نمونه کتابخانه مالک آن است و آن را بازیافت می‌کند. قرارداد این است: فوراً کپی کنید، هرگز آزاد نکنید

خط زمانی PDF Library for Delphi که کپی فوری اشاره‌گر قرضی DLGetPageText را با نگه‌داشتن آن تا زمانی که کتابخانه بافر زیرین را بازیافت می‌کند مقایسه می‌کند
اشاره‌گرهای نویسه برگردانده‌شده حافظه‌ای را قرض می‌گیرند که نمونه بازیافت می‌کند، پس کپی باید پیش از فراخوانی بعدی کتابخانه انجام شود
var
  P: PWideChar;
  PageText: string;
begin
  P := DLGetPageText(Inst, 7);   // اشاره‌گر به بافری که کتابخانه مالک آن است
  PageText := P;                 // همین حالا کپی کنید؛ فراخوانی بعدی ممکن است بافر را دوباره استفاده کند
end;

در C# این یعنی مارشال‌کردن IntPtr به یک رشته مدیریت‌شده پیش از فراخوانی بعدی کتابخانه. در ctypes پایتون یعنی بلافاصله رشته wide را از اشاره‌گر برش بزنید. اشاره‌گر خام را بین فراخوانی‌ها نگه دارید و باگی نوشته‌اید که از هر تست واحدی می‌گذرد و سپس اولین باری که دو درخواست در محیط تولید هم‌پوشانی پیدا می‌کنند شکست می‌خورد، چون فراخوانی دوم بافری را بازیافت کرده که اولی هنوز داشت می‌خواند. همان قاعده مالکیت در جهت مخالف برای callbackهایی که از طریق DLSetProgressCallback ثبت می‌شوند هم برقرار است. هر اشاره‌گری که کتابخانه به callback شما می‌دهد فقط برای بدنه همان callback معتبر است، و خودِ شیء callback باید تا زمانی که نمونه ممکن است هنوز آن را فراخوانی کند زنده بماند (در یک میزبان با garbage collection، pin شده). یک delegate که وسط کار جمع‌آوری شده منبع کلاسیک آن نقض دسترسی «تصادفی» است که در یک اتصال .NET ظاهر می‌شود که ماه‌ها بی‌مشکل کار کرده بود

یک آزمون دودی (smoke test) درون خودِ اتصال بسازید و آن را پیش از عرضه هر مجموعه اعلان تولیدشده اجرا کنید. از هر دسته‌ای که معمولاً اشتباهات ABI را آشکار می‌کند یک فراخوانی تمرین کنید: یک تابع بدون پارامتر مانند DLCreateLibrary برای اثبات این‌که قرارداد درست است، یک تابع رشته‌ورودی که مسیری با نویسه‌های غیر ASCII به آن داده شده برای اثبات این‌که کدگذاری درست است، یک تابع رشته‌خروجی برای اثبات این‌که مدیریت بافر قرضی درست است، و یک عملیات که عمداً شکست می‌خورد تا ببینید یک خطا چگونه به میزبان شما می‌رسد. این پانزده دقیقه کار است و خطاهای قرارداد فراخوانی و کدگذاری‌ای را می‌گیرد که در غیر این صورت ماه‌ها بعد به‌صورت crash dump یک مشتری از راه می‌رسیدند

جدول دو در دو PDF Library for Delphi از کاوشگرهای آزمون دودی اتصال که قرارداد فراخوانی، کدگذاری رشته، بافرهای قرضی و آشکارسازی خرابی را پوشش می‌دهند
چهار کاوشگر ارزان، خطاهای قرارداد، کدگذاری و مالکیت را پیش از آن‌که اعلان‌های تولیدشده به دستگاه مشتری برسند می‌گیرند

مورد ctypes پایتون، به‌طور مشخص

ctypes پایتون اتصالی است که بیش از همه دست‌ساز می‌بینم، و نمایش شکاف میان پلتفرم‌ها را آسان می‌کند. روی ویندوز، کتابخانه را با ctypes.WinDLL بارگذاری کنید تا ctypes قرارداد Stdcall را اعمال کند، توابع W بدون پسوند را متصل کنید و هر پارامتر رشته‌ای را c_wchar_p اعلان کنید. روی macOS، آن را با ctypes.CDLL برای Cdecl بارگذاری کنید، همان فهرست تابع را نگه دارید و نام‌ها را بدون زیرخط پیشوندی حل کنید. بیشتر لایه‌های FFI، از جمله ctypes، قرارداد زیرخط را روی macOS برایتان به‌طور خودکار جبران می‌کنند، اما این همان یک فرضی است که باید پیش از تولید صدها اعلان روی آن، با یک فراخوانی حل‌شده تأیید کنید

دو پرسش استقرار در پی کار اتصال می‌آیند و پاسخ‌های روشنی دارند. DLL ساده هیچ ثبتی لازم ندارد: regsvr32 فقط برای ساخت ActiveX کاربرد دارد و DLL با کپی فایل عرضه می‌شود، که دلیل اصلی ترجیح آن برای سرویس‌های ویندوز و کانتینرهاست، جایی که ترجیح می‌دهید اصلاً به رجیستری دست نزنید. ایمنی رشته‌ای به قاعده‌ای که پیش‌تر مطرح شد فروکاسته می‌شود، یک نمونه به ازای هر رشته. هندل نمونه هر تکه از حالت تغییرپذیری را که موتور دنبال می‌کند نگه می‌دارد، سند انتخاب‌شده، گزینه‌های رندر، تنظیمات استخراج، پس دو رشته‌ای که یک نمونه را به اشتراک می‌گذارند حالت یکدیگر را درهم می‌آمیزند، حتی وقتی هر فراخوانی منفرد موفقیت برمی‌گرداند

وقتی یک اتصال محکم شد، عملیات آن سوی آن دقیقاً همان‌هایی هستند که مقالات دلفی به‌تفصیل پوشش می‌دهند، از جمله اعمال و ممیزی رمزگذاری PDF و استخراج متن و تصویر از سندهای موجود

دانلودهای باینری هر سه لایه یکپارچه‌سازی همراه کتابخانه عرضه می‌شوند؛ برای نسخه‌ها و مجوزدهی به صفحه محصول PDF Library for Delphi مراجعه کنید