تربط PDFlibPas 3.538.0 مُرمّز JBIG2 الخارجي ربطًا ساكنًا داخل برامج Free Pascal وLazarus. يضيف المشروع الوحدة PDFlibJBIG2EncC، وهي الوحدة نفسها التي يستخدمها Delphi وC++Builder أصلًا، وينتهي الأمر بالمُرمّز داخل الملف التنفيذي من دون الحاجة إلى شحن أي ملف إضافي بجانبه. وهذا يقلب الاستنتاج السابق بشأن هذه الميزة، إذ كان يُعتقد أن Free Pascal لا يستطيع الوصول إلى المُرمّز الخارجي إلا عبر DLL
لماذا بدت DLL الخيار الوحيد؟
بدت DLL الخيار الوحيد لأن ثلاثة مسارات للربط فشلت بثلاث طرق لا علاقة بينها، ولم يصل أي مفتاح في المصرّف إلى أي مشكلة منها. فالرابط الداخلي يرفض أقسام COMDAT الترابطية رفضًا مباشرًا. أما الربط الخارجي عبر binutils المرفقة فينهار داخل جمع الأقسام غير المستخدمة، وهي خطوة يمررها Free Pascal بلا شرط على هدف Windows ذي 64 بت. ولا تستطيع binutils الأحدث معالجة برنامج ربط Free Pascal أصلًا. وإعادة بناء جانب C++ باستخدام سلسلة الأدوات الأخرى لا تفعل أكثر من استبدال رفض برفض آخر، لأن إنشاء القوالب وعمليات inline يولّد رموزًا خارجية ضعيفة بطبيعته، ويبلغ Free Pascal عنها بصيغة Unsupported COFF symbol type 105. لا يوجد خطأ في أي من هذه الأدلة، كما أن الشرح السابق لخلفيات مُرمّز JBIG2 ورابط Free Pascal يمر بكل طريق مسدود بصيغة ما زالت قابلة لإعادة الإنتاج حتى اليوم. الخطأ كان في افتراض موضع الإصلاح. فكل محاولة مرت عبر المصرّف أو الرابط، ولا يستطيع أي منهما تغيير ما يحتويه ملف الكائنات أصلًا. كان ملف الكائنات هو المشكلة طوال الوقت. يقرأ ObjConv صيغة COFF ويكتبها، ولكل بنية يتعثر فيها Free Pascal مكافئ ميكانيكي يقبله
الخطأ الذي لا يسمّي سببه أبدًا
ينفذ رابط Free Pascal الداخلي جزءًا من COMDAT من نوع pick-any فقط، وهذا التنفيذ الناقص هو أصعب نقطة في التشخيص هنا. فهو يطوي التعريفات المكررة فعلًا كما تقتضي الصيغة. لكن TExeOutput.RemoveUnreferencedSections يعيد التوجيه عبر exesymbol إلى التعريف الفائز عند تعليم الأقسام بأنها مستخدمة، بينما يقرأ TCoffexeoutput.DoRelocationFixup قيمة objreloc.symbol.objsection مباشرة. وعندما يشير قسم مستخدم إلى رمز يعرّفه ملف الكائنات نفسه داخل نسخة خسرت عملية الطي، تصبح المرحلتان ناظرتين إلى قسمين مختلفين، ويتوقف الربط عند Internal error 200603061
قارن ذلك بالقيدين المحيطين به. فعبارة Unsupported COFF symbol type 105 تعني رمزًا خارجيًا ضعيفًا. وعبارة Associative or exact match COMDAT sections are not yet supported تعني COMDAT ترابطيًا، بل وتسمي الرمز المتسبب في المشكلة. أما الخطأ الداخلي 200603061 فلا يقول شيئًا: لا اسم رمز، ولا اسم قسم، ولا اسم ملف، ولا معلومات عن المرحلة. وهو أيضًا الحالة المعتادة لا حالة هامشية، لأن MSVC يضع كل literal نصي وكل إنشاء inline أو template داخل COMDAT من نوع pick-any، وقد نفذ الرابط 2656 عملية طي عبر كائنات المُرمّز البالغ عددها 186. ويُبقي البناء باستخدام /Gy- الدوال العادية خارج أقسام COMDAT الخاصة بكل دالة، لكنه يترك literals النصية وإنشاءات القوالب في أماكنها تمامًا
لماذا يبدو إخراج رموز CRT البديلة وكأن آخر رمز هو الذي أفسد البناء؟
لأن الرابط لا يصل إلى مرحلة الإصلاح إلا بعد حل كل الرموز. وما دام أي رمز مفقودًا، تنتهي العملية مبكرًا برسالة Undefined symbol ولا تتاح لمشكلة COMDAT فرصة الظهور. وعند إكمال آخر دالة بديلة من وقت تشغيل C، تتقدم عملية الربط مرحلة واحدة لتصطدم مباشرة بالخطأ الداخلي 200603061. لذلك تكون العلامة الظاهرة مضللة منهجيًا: فعند إضافة أجسام Pascal للرموز C المشار إليها واحدًا بعد آخر، يبدو دائمًا أن الإضافة الأخيرة أفسدت البناء، أو أن عتبة ما قرب مئة دالة بديلة قد تم تجاوزها. لا شيء من ذلك صحيح. فالرمز الذي أُضيف أخيرًا وعدد الرموز التي أُضيفت إجمالًا غير مهمين، لأن الفشل كان كامنًا منذ أول ملف كائن ولم يصبح قابلًا للوصول إلا بعد نجاح الحل. وعندما يغيّر الرابط شكواه بعد إصلاح مشكلة لا علاقة لها به، اسأل أولًا هل تقدمت مرحلة واحدة بدل أن تكون قد سببت تراجعًا
الإصلاح هو تمريرة ObjConv واحدة، لا مفتاحًا في المصرّف
الإصلاح الكامل هو أمر معالجة لاحقة واحد يُشغّل على كل كائن مُترجم: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. أُضيفت ثلاثة من هذه الخيارات من أجل هذا العمل. يحل -xw رموز IMAGE_SYM_CLASS_WEAK_EXTERNAL إلى رموز خارجية عادية. ويطبّع -xn رموز IMAGE_SYM_CLASS_NULL مثل _fltused، التي يبلغ Free Pascal عنها بصيغة Unsupported COFF symbol type 0. أما -xc فهو الذي ينفذ العمل الأساسي: إذ يحوّل كل قسم COMDAT إلى قسم عادي، ويجعل الرموز التي يعرّفها ساكنة. وبذلك يزيل سبب الفشل لأنه يلغي القرار نفسه؛ فلا توجد أقسام COMDAT يعني عدم وجود طي، ولا نسخة فائزة تعيد إليها مرحلة توجيهًا وتفشل مرحلة أخرى في العثور عليها، كما تختفي معه أقسام فكّ التكديس الترابطية .pdata و.xdata. الكلفة حقيقية لكنها صغيرة، إذ تبقى الآن كل النسخ التي كان يمكن دمجها بصورة مشروعة
يعالج تغيير بادئة -np:__imp_:pdflibimp_ تصادمًا منفصلًا. يستدعي MSVC واجهات Win32 المستوردة عبر خلايا توجيه تحمل أسماء __imp_*، بينما يحتفظ Free Pascal بهذه البادئة لآلية الاستيراد الخاصة به، ويؤدي تعريف أحد هذه الأسماء مباشرة إلى الخطأ الداخلي 200603061 نفسه. ويتيح تغيير الاسم لجانب Pascal نشر الخلايا كمتغيرات عادية وملئها أثناء التشغيل. تُترجم الكائنات نفسها مع تفعيل أعلام الربط الساكن /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- وتعطيل مرمّزات الصور، ولذلك تحتاج مسارات إدخال وإخراج الملفات والمرمّزات غير المستخدمة إلى عدد أقل بكثير من الدوال البديلة الخاصة بالربط. وتوضع هذه الكائنات في Lib\thirdparty\Win64f، بينما يواصل مسار Delphi وC++Builder ربط مجموعته الخاصة Win64x من دون تغيير، وهو الناتج الصحيح لإصلاح قابلية نقل محصور في سلسلة أدوات واحدة
ما الذي يجب على جانب Pascal تصديره أيضًا؟
يحل Free Pascal استيراد كائن C باسم الرمز، ولذلك يجب أن تحمل كل دالة Pascal تحل محل نقطة دخول C عبارة public name صريحة. أما Delphi فيأخذ اسم الدالة باعتباره اسم الرمز ولا يحتاج إلى أي عبارة، ولهذا تخدم وحدة واحدة المصرّفين مع وضع العبارات تحت {$IFDEF FPC}. الفخ هو أن إعلان external 'msvcrt.dll' لا يفي بأي شيء: فهو ينشئ استيرادًا، لكنه لا ينشئ تعريفًا يمكن لكائن مربوط أن يرتبط به. لا بد من وجود جسم التحويل نفسه
// الإعلان الخارجي ينشئ استيرادًا فقط. لا يمكن لأي كائن مربوط
// الارتباط به.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// جسم Pascal المنشور تحت اسم رمز C المطابق هو ما ترتبط به
// مجموعة الكائنات فعليًا.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
تكسر نقاط الدخول ذات المعاملات المتغيرة هذا النمط، لأن غلاف Pascal لا يستطيع تمرير varargs الخاصة به إلى دالة مستدعاة أخرى ذات varargs. والحل هو التوقف عن كونها غلافًا: صدّر دالة عارية تحت اسم C، ثم اقفز قفزة نهائية إلى التنفيذ الحقيقي مع إبقاء سجلات المعاملات والمكدس كما رتّبها المستدعي تمامًا. وتتعامل طبقة JPEG 2000 مع snprintf وvsnprintf بهذه الطريقة أصلًا، إذ تقفز إلى صيغ msvcrt ذات البادئة السفلية لأن UCRT وحدها تصدّر الأسماء العادية. وينشأ قيد مرتبط من الخطأ الداخلي نفسه: تُملأ خلايا الاستيراد المعاد تسميتها من قسم initialization عبر GetModuleHandleA وGetProcAddress بدل المُهيئات الساكنة، لأن أخذ عنوان دالة مستوردة داخل مُهيئ يجعل المصرّف يصدر إصلاحًا لا يستطيع معالجته ويفشل من جديد بالخطأ 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// لا يمكن تمرير Varargs من غلاف Pascal، لذلك يقفز الرمز
// المصدّر قفزة نهائية مع إبقاء إطار المستدعي كما أعدّه.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
ما الذي يفعله مشروع Free Pascal بصورة مختلفة الآن؟
لا شيء يتجاوز اسم الوحدة في عبارة uses، ولم يعد هناك ملف يجب نشره. تسجل الخلفية نفسها من قسم initialization الخاص بها عبر RegisterJBIG2EncoderBackend، ويطلبها المستدعون بالطريقة نفسها كما قبل: عبر بت الخيارات PDF_JBIG2_OPTION_EXTERNAL_ENCODER الذي قيمته 4، أو عبر المعامل UseExternalEncoder في نقاط الدخول الموسعة. ويظل طلبها تفضيلًا لا ضمانًا، إذ يعود البناء الذي أغفل الوحدة بصمت إلى مرمّز Pascal الأصلي من نوع MMR وينتج ملفات أكبر بدلًا من إصدار خطأ
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi وC++Builder، وFree Pascal منذ 3.538.0
var
Pdf: TPDFlib;
Scan: TStream;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
try
// Interpolate وSymbolExtract وUseExternalEncoder وSkipBlackDots،
// BlackDotSize وLossyLevel
ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
finally
Scan.Free;
end;
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
هناك حدّان ينبغي ذكرهما بوضوح. توجد مجموعة كائنات Win64 فقط، ولذلك تبلغ نقطة دخول الترميز الخارجي عن فشلها على كل هدف Free Pascal آخر، ويتولى المرمّز الأصلي من Pascal العمل. كما أن اختبار التراجع الذي يحكم كل هذا هو مقارنة تصيير لا فحص حجم: فكلا المرمّزين غير فقدي بالنسبة إلى المصدر نفسه، ولذلك تُصيّر مخرجاتهما وتقارن بايتًا ببايت، وقد نجحت مجموعة اختبارات Lazarus في 26 اختبارًا من أصل 26 بما فيها هذا الاختبار. أما مقارنة أحجام التدفقات المضغوطة فلن تثبت شيئًا، لأن الصفحة المقلوبة تنضغط بحجم يقارب حجم الصفحة الصحيحة
والدرس الأوسع يتجاوز JBIG2. تكون DLL هي الشكل الصحيح عندما تكون الحدود ديناميكية فعلًا، وهو ما تخدمه واجهات تكامل DLL وActiveX وdylib، لكنها تكون الشكل الخاطئ عندما لا تعدو كونها التفافًا مؤقتًا حول قارئ COFF، لأنها تضيف ملفًا إلى كل برنامج تثبيت، ومسار بحث إلى كل عملية نشر، ونمط فشل لاختلاف الإصدارات لا يمكن أن يسببه الربط الساكن. كما أن المرحلة السابقة مهمة، لأن طريقة إنتاج الصورة ثنائية المستوى تؤثر في الحجم النهائي أكثر من المرمّز نفسه، وتغطي عملية التصيير أحادية اللون المعتمدة على المناطق في Delphi ذلك النصف من المسار. وترد تغطية سلاسل الأدوات، ومجموعات الكائنات الخاصة بكل مصرّف، والأهداف المدعومة في صفحة منتج losLab PDF Developer Library