مقال تقني

ارتباطات PDFlibPas DLL و ActiveX و dylib: استدعاء محرك PDF واحد من أي لغة

إليك مشكلة تظهر بمجرد أن تغادر مكتبة PDF لغتها الأصلية. لديك ارتباط يعمل بشكل مثالي من C# على Windows. أنت بحاجة إلى نفس الاستدعاءات من Python على macOS، لذلك تقوم بنسخ ملف التصريحات الخاص بـ Windows، وتبديل اسم الملف الثنائي، وتشغيله. يتم حل كل رمز. يعيد الاستدعاء الأول بيانات غير صالحة (garbage)، ويتعطل الثاني مع انتهاك وصول (access violation)، ولم يتغير أي من كود PDF الخاص بك. يكمن الخطأ في طبقة واحدة أسفل PDF: تستخدم عمليات التصدير في Windows اصطلاح Stdcall، وتُصدِّر dylib في macOS نفس الدوال كـ Cdecl مع شرطة سفلية بادئة، وتصريح الدالة الأجنبية الذي يخطئ في أي من التفاصيل يفسد المكدس قبل فتح مستند واحد

تنبع هذه الفئة الكاملة من الفشل من قرار تصميم واحد يستحق الفهم مسبقًا. تقوم PDFlibPas، وهو محرك PDF متوفر المصدر من losLab لـ Delphi و C++Builder، بتغليف نموذج الكائنات الخاص به بالكامل في فئة واجهة مسطحة واحدة، وهي TPDFlib، ثم تقوم بشحن هذه الواجهة في ثلاثة أشكال ثنائية: Windows DLL بحوالي 1250 دالة مصدرة، وكائن أتمتة COM/ActiveX، و macOS dylib. دلالات PDF متطابقة عبر الثلاثة. الجزء الذي يعضك يعيش في واجهة التطبيق الثنائية (ABI) الموجودة بالأسفل: اصطلاحات الاستدعاء، وترميزات السلاسل النصية، وملكية المقابض، وأي جانب مسموح له بتحرير أي مخزن مؤقت

واجهة واحدة، ثلاثة أشكال ثنائية

يحتوي كل دالة عامة لـ TPDFlib على نظير مسطح يسمى DL بالإضافة إلى اسم التابع. تصبح LoadFromFile هي DLLoadFromFile، وتصبح Encrypt هي DLEncrypt، وتصبح NewSignProcessFromFile هي DLNewSignProcessFromFile. المعلمة الأولى لتقريبًا كل تصدير هي InstanceID يتم إرجاعها بواسطة DLCreateLibrary، لتحل محل مرجع الكائن الذي قد يحتفظ به مستدعي دلفي. استوعب هذا التعيين مبكرًا. هذا يعني أن مرجع واجهة برمجة تطبيقات دلفي يتضاعف كوثائق لكل لغة أخرى: كل ما يمكن للفئة القيام به، يمكن لـ DLL القيام به تحت اسم متوقع، ويمكنك قراءة توقيع تابع باسكال لمعرفة الاستدعاء الذي تحتاجه من Python أو C#

ينتج بناء Windows كلاً من PDFlibDLL32.dll و PDFlibDLL64.dll؛ اختر الذي يتطابق مع بنية عملية المضيف الخاصة بك (bitness)، لأن عملية Java أو .NET ببنية 64 بت لا يمكنها تحميل مكتبة 32 بت بغض النظر عن شكل التصريح

Windows: مثيلات Stdcall وأزواج الدوال W/A

يوجد كل تصدير يأخذ سلسلة نصية مرتين. نسخة واسعة تأخذ PWideChar (UTF-16، الملاءمة الطبيعية لـ .NET و Java و c_wchar_p في Python)، ونسخة ملحقة بـ A تأخذ 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 في كل مكان ولا تلمس عائلة A مرة أخرى أبدًا

macOS: نفس الأسماء، ABI مختلف

تقوم dylib بتصدير نفس مجموعة دوال DL مع تغييرين منهجيين. اصطلاح الاستدعاء هو Cdecl بدلاً من Stdcall، ويحمل كل اسم تصدير شرطة سفلية بادئة (_DLCreateLibrary، و _DLLoadFromFile، وما إلى ذلك). كلا التغييرين ميكانيكيان بحتان، مما يجعلهما مثاليين لارتباط تم إنشاؤه وخطيرين لنسخة محررة يدويًا من ملف Windows. احتفظ بقائمة دوال أساسية واحدة وقم بإصدار تصريحات لكل نظام أساسي منها إذا كانت أدواتك تسمح بذلك. تخطى ذلك وستحصل على فساد المكدس الدقيق الموصوف في أعلى هذه الصفحة، والذي لا يتكرر إلا على النظام الأساسي الذي تصادف أن يمارسه نظام التكامل المستمر (CI) الخاص بك بشكل أقل

مضيفو COM و ActiveX: حمولات Safecall و Olevariant

بالنسبة لـ VB.NET و C# و VBScript ومضيفي الأتمتة القدامى، يقوم بناء OCX بتغليف نفس الواجهة في كائن أتمتة IDispatch، وهو IPDFlibrary، مع الإعلان عن كل طريقة كـ Safecall. يغير هذا الاصطلاح كيفية وصول الأخطاء إليك. يُترجم Safecall الفشل الداخلي إلى COM HRESULT، لذلك يلتقط مستدعي C# استثناءً حيث كانت واجهة DLL المسطحة ستُرجع عددًا صحيحًا هادئًا كان على المستدعي أن يتذكر التحقق منه. نفس العملية، تعبيران عن الفشل، اعتمادًا على الملف الثنائي الذي قمت بتحميله

تتبع البيانات الثنائية قاعدة ثانية خاصة بـ COM. واجهة الأتمتة لا تحتوي على معلمات مؤشر (pointer parameters) على الإطلاق. أي شيء ثنائي، مثل بايتات الصور الداخلة أو بايتات PDF الخارجة، يعبر الحدود كـ Olevariant من خلال طرق مثل AddImageFromVariant و AppendToVariant. يُعد تجميع مصفوفة بايت في متغير (variant) سطرًا واحدًا في .NET. حاول تسليمه مؤشرًا خامًا بدلاً من ذلك، بناءً على منطق أنها نفس العملية على أي حال، وترفض طبقة الإرسال (dispatch layer) الاستدعاء أو تشوهه. تعرقل تفصيلة تسجيل أخرى عمليات النشر: تسجيل COM يعتمد على بنية البت (per-bitness)، لذا فإن OCX المسجل بواسطة regsvr32 ذو 32 بت غير مرئي لمضيف ذي 64 بت. يظهر عدم التطابق هذا كرسالة غير مفيدة شهيرة "لم يتم تسجيل الفئة" (class not registered) على جهاز العميل، بعد فترة طويلة من مغادرته جهازك

انضباط المقابض: المثيلات تملك المستندات

تعمل واجهة برمجة التطبيقات المسطحة (flat API) على مقابض صحيحة (integer handles). تُرجع DLCreateLibrary مثيلاً (instance). يؤدي تحميل ملف إلى إرجاع معرف مستند (document ID) داخل ذلك المثيل. تُرجع عمليات التوقيع، وقوائم السلاسل، وملفات الوصول المباشر مقابضها الصحيحة الخاصة بها، وكلها ضمن نطاق نفس المثيل. تبدو دورة الحياة متشابهة من أي مضيف FFI، وهي معروضة هنا بلغة باسكال لأن قراءتها واضحة:

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 هو استدعاء التنظيف الوحيد الذي تحتاجه بشكل صارم، لأنه يهدم كل مقبض مستند وعملية تحت المثيل بضربة واحدة. في برنامج نصي قصير، هذا يكفي. في خدمة طويلة الأمد، يصبح الأمر بمثابة تسرب بطيء مع احتفال إضافي، لذا قم بتحرير المستندات فور الانتهاء منها بدلاً من السماح لها بالتراكم حتى يموت المثيل. يُعد المثيل أيضًا الوحدة الطبيعية لعزل الخيوط (thread isolation). امنح كل خيط عامل InstanceID الخاص به، ولا تشارك أبدًا واحدًا عبر الخيوط بدون قفل خارجي، لنفس السبب الذي يجعلك لا تشارك أبدًا كائن TPDFlib واحد بين الخيوط

السلاسل المعادة مُستعارة وليست مملوكة

الدوال التي ترجع نصًا، مثل DLGetPageText، تُسلم PWideChar أو PAnsiChar الذي يشير إلى مخزن مؤقت (buffer) يمتلكه ويعيد تدويره مثيل المكتبة. العقد هو: انسخ فورًا، ولا تقم بالتحرير أبدًا

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#، يعني ذلك تجميع (marshaling) الـ IntPtr في سلسلة مدارة قبل استدعاء المكتبة التالي. في ctypes الخاصة بـ Python، يعني ذلك اقتطاع السلسلة الواسعة من المؤشر على الفور. إذا احتفظت بالمؤشر الخام عبر الاستدعاءات، تكون قد كتبت خطأً (bug) يتجاوز كل اختبارات الوحدة ثم يفشل في المرة الأولى التي يتداخل فيها طلبان في الإنتاج، لأن الاستدعاء الثاني أعاد تدوير المخزن المؤقت الذي كان الأول لا يزال يقرأه. تسري نفس قاعدة الملكية في الاتجاه الآخر لعمليات الاستدعاء (callbacks) المسجلة عبر DLSetProgressCallback. أي مؤشر تسلمه المكتبة إلى الاستدعاء الخاص بك يكون صالحًا فقط لجسم ذلك الاستدعاء، ويجب أن يظل كائن الاستدعاء نفسه حيًا (مثبتًا، في مضيف مجمع للقمامة) طالما أن المثيل قد لا يزال يستدعيه. يُعد المفوض (delegate) الذي تم جمعه في منتصف المهمة المصدر التقليدي لانتهاك الوصول "العشوائي" الذي يظهر في ارتباط .NET الذي كان يعمل بنظافة لعدة أشهر

قم ببناء اختبار دخان (smoke test) في الارتباط نفسه، وقم بتشغيله قبل شحن أي مجموعة تصريحات تم إنشاؤها. قم بممارسة استدعاء واحد من كل فئة تميل إلى كشف أخطاء ABI: دالة بدون معلمات مثل DLCreateLibrary لإثبات صحة الاصطلاح، ودالة إدخال سلسلة نصية (string-in) تم تغذيتها بمسار يحتوي على أحرف غير ASCII لإثبات صحة الترميز، ودالة إخراج سلسلة نصية (string-out) لإثبات صحة التعامل مع المخزن المؤقت المستعار، وعملية واحدة تفشل عن قصد حتى تتمكن من مشاهدة كيف يصل الخطأ إلى مضيفك. هذا العمل يستغرق خمس عشرة دقيقة، وهو يلتقط أخطاء اصطلاح الاستدعاء والترميز التي لولا ذلك لوصلت بعد أشهر كتفريغ أعطال (crash dump) من العميل

حالة Python ctypes، بشكل ملموس

ارتباط Python ctypes هو الارتباط الذي أرى أنه يتم كتابته يدويًا في أغلب الأحيان، وهو يجعل توضيح الانقسام عبر الأنظمة الأساسية أمرًا سهلاً. في نظام Windows، قم بتحميل المكتبة باستخدام ctypes.WinDLL حتى يطبق ctypes اصطلاح Stdcall، واربط دوال W غير الملحقة، وقم بالتصريح عن كل معلمة سلسلة نصية كـ c_wchar_p. على نظام macOS، قم بتحميله باستخدام ctypes.CDLL لـ Cdecl، واحتفظ بقائمة الدوال المتطابقة، وقم بحل الأسماء بدون الشرطة السفلية البادئة. معظم طبقات FFI، بما في ذلك ctypes، تقوم بطي اصطلاح الشرطة السفلية نيابة عنك في macOS، ولكن هذا هو الافتراض الوحيد الذي يجب تأكيده باستدعاء واحد تم حله قبل إنشاء مئات التصريحات فوقه

يتتبع سؤالان حول النشر أعمال الارتباط ولهما إجابات واضحة. لا تحتاج DLL البسيطة إلى أي تسجيل: ينطبق regsvr32 فقط على بناء ActiveX، ويتم شحن DLL عن طريق نسخ الملفات، وهو السبب الرئيسي لتفضيله لخدمات وحاويات Windows حيث تفضل عدم لمس السجل (registry) على الإطلاق. تنخفض سلامة الخيوط (Thread safety) إلى القاعدة المعمول بها بالفعل أعلاه، وهي مثيل واحد لكل خيط. يحمل مقبض المثيل كل جزء من الحالة القابلة للتغيير التي يتتبعها المحرك، والمستند المحدد، وخيارات العرض، وإعدادات الاستخراج، لذلك فإن خيطين يتشاركان في مثيل يتداخلان في حالة بعضهما البعض حتى عندما يُرجع كل استدعاء فردي نجاحًا

بمجرد أن يصبح الارتباط صلبًا، فإن العمليات على الجانب الآخر منه هي بالضبط تلك التي تغطيها مقالات دلفي بتعمق، بما في ذلك تطبيق وتدقيق تشفير PDF و استخراج النص والصور من المستندات الموجودة

تُشحن التنزيلات الثنائية لجميع طبقات التكامل الثلاث مع المكتبة؛ راجع صفحة منتج PDFlibPas لمعرفة الإصدارات والترخيص