مقاله فنی

تشخیص شکست‌های بی‌صدای stub در یک کتابخانه PDF پاسکالی

وقتی یک کتابخانه دلفی یک پیکربندی بیلد بدون فریمورک بصری پیدا می‌کند، کلاس‌های جایگزین همان جایی هستند که باگ‌ها زندگی می‌کنند. نه پلتفرم، نه کامپایلر: جانشین‌ها. PDFlibPas یک لایه گرافیک دارد که برای بیلدهای بدون VCL معادل‌های bitmap، canvas، فونت، متافایل و چاپگر فراهم می‌کند، و پورت‌کردنش به Free Pascal هر حالت شکستی که یک جانشین می‌تواند داشته باشد را بیرون کشید. آن‌ها به‌خوبی بر اساس هزینه تشخیص مرتب می‌شوند، و ترتیب برعکس چیزی است که شهود پیشنهاد می‌کند

جانشینی که استثنا پرتاب می‌کند ارزان پیدا می‌شود؛ استثنا نام متد را می‌گوید. جانشینی که داده خالی برمی‌گرداند گران است، چون شکست چند لایه دور از علتش ظاهر می‌شود. جانشینی که موفقیت برمی‌گرداند بدترین همه است، چون کد بازگشت معتبر است، کد خطا صفر است، هیچ استثنایی پرتاب نمی‌شود، و تنها مدرک اینکه چیزی اشتباه رفته در بایت‌هایی است که بیرون آمده

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

شکل سه: یک شناسه تصویر معتبر روی یک XObject خالی

مبدل متافایل وکتوری در پیکربندی non-VCL یک بدنه رویه خالی بود. همه چیز بالای آن به کارش ادامه می‌داد. نقاط ورود واردات EMF و نقطه ورود گرفتن از canvas تا انتها اجرا می‌شدند و یک شناسه تصویر قانونی برمی‌گرداندند، که فراخواننده بعد آن را روی یک صفحه می‌گذاشت. چیزی که در فایل فرود می‌آمد یک form XObject با طول محتوای صفر بود. صفحه سفید رندر می‌شد

هیچ‌چیز مشکلی را گزارش نمی‌کرد، و این شامل برنامه نمایشی خود کتابخانه برای این ویژگی هم می‌شود، که یک صفحه خالی می‌کشید و نمی‌فهمید. هیچ مقدار بازگشت شکست‌خورده‌ای برای بررسی نبود، چون رشته فراخوانی‌ها واقعاً همگی موفق بودند؛ تنها چیزی که غلط بود اندازه جریان تولیدشده بود. تشخیص این رده عیب یعنی پرسیدن یک پرسش دیگر: نه «آیا فراخوانی شکست خورد» بلکه «آیا مصنوع موجه است». یک form XObject با طول صفر، یک تصویر با صفر پیکسل، یک صفحه با صفر بایت محتوا، این‌ها همان ادعاهایی هستند که آن را می‌گیرند

fix دو نیمه دارد و نیمه دوم به‌آسانی فراموش می‌شود. اول، پیاده‌سازی خالی را پرتاب‌کننده کنید، تا شکست اصلاً یک مجرا داشته باشد. دوم، آن استثنا را در کارخانه تصویر به یک نتیجه null تبدیل کنید و در دو جایی که شناسه تصویر مصرف می‌شود null check اضافه کنید، چون در غیر این صورت «شکست تمیز» مستقیم به یک access violation تبدیل می‌شود وقتی درخت صفحه به هیچ‌چیز ارجاع‌گشایی می‌کند. یک stub پرتاب‌کننده فقط وقتی بهبود است که فراخوانندگان برای شکستی آماده باشند که قبلاً هرگز قادر به دریافتش نبودند

شکل دو: داده خالی، سه لایه دور از crash

جانشین canvas متافایل ابعاد فیزیکی‌اش را پر نمی‌کرد. آن مقدار در یک محاسبه هندسه صفحه تقسیم می‌شود، پس محاسبه صفر تولید می‌کرد، پس محاسبه bounding box بر صفر تقسیم می‌شد. یک handler استثنای لخت آن را قورت می‌داد، کارخانه تصویر یک نتیجه null برمی‌گرداند، و access violation نهایتاً در درخت صفحه اتفاق می‌افتاد وقتی null استفاده می‌شد. سه لایه میان علت و نشانه، با یک handler استثنا در وسط که مدرک را پاک می‌کند

زنجیره شکست جانشین canvas متافایل خالی در PDFlibPas که سه لایه پس از تقسیم بر صفر به یک access violation می‌رسد
بعد خالی یک محاسبه هندسه را به صفر می‌رساند، handler لخت استثنا را پاک می‌کند، و شناسه null درخت صفحه را crash می‌کند

همان یونیت دو نمونه دیگر از این الگو داشت. کلاس فونت بدنه‌های Assign و سازنده خالی داشت، که بیشتر از ظاهرش مهم است چون پراپرتی فونت canvas فقط‌خواندنی است: انتساب به آن تنها راه رساندن یک فونت است، پس پیاده‌سازی خالی انتخاب فونت را بی‌سروصدا بی‌اثر می‌کند و متن با هر چه پیش‌فرض بود بیرون می‌آید. و یک مقدار pixel-per-inch صفر هر فراخواننده‌ای که canvas را از متریک‌های فونت اندازه می‌گرفت به تولید یک canvas صفردرصفر می‌رساند، که صفحه خالی و بازگشت موفقیت می‌دهد

// شکلی که در یک یونیت جانشین باید دنبالش گشت: متدی که نه
// پرتاب می‌کند نه کاری می‌کند. هر دو کامپایل می‌شوند و هر دو
// «موفقیت» بدون خروجی تولید می‌کنند
procedure TMetafileCanvasStandIn.Create(...);
begin
  // بدون فراخوانی inherited، بدون مقداردهی اولیه فیلد
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // و bitmap همچنان خالی است
end;

ساختار wide که فقط نویسه اول را نگه می‌دارد

این یکی اصلاً مشکل جانشین نیست، ولی به همان کاتالوگ تعلق دارد چون نشانه به همان اندازه از علت دور است. ساختار شمارش چاپگر با هر دوازده عضو رشته‌ای‌اش به‌صورت اشاره‌گر به نویسه‌های تک‌بایتی اعلان شده بود، در حالی که تابعی که آن را پر می‌کند گونه نویسه‌پهن API شمارش است

اندازه اشاره‌گرها یکسان است، پس چیدمان ساختار درست است و هیچ crashی در کار نیست. آنچه به‌جای آن می‌شود این است که خواندن یک رشته UTF-16 به‌عنوان رشته تک‌بایتی در اولین بایت صفر متوقف می‌شود، که برای هر نام چاپگر ASCII نیمه بالایی نویسه دوم است. هر نام چاپگر دقیقاً یک نویسه برگشت. در پایین‌دست، اعتبارسنجی نام شکست خورد، ساخت چاپگر شکست خورد و چاپ برای هر چاپگر واقعی روی دستگاه شکست خورد، و هیچ‌کدام از آن نشانه‌ها به یک اعلان ساختار اشاره نمی‌کند

نام چاپگر UTF-16 بریده‌شده به یک نویسه پس از آنکه یک ساختار wide وین۳۲ با اعضای PAnsiChar به‌جای PWideChar اعلان شد
اعضای تک‌بایتی یک نام UTF-16 را فقط تا اولین بایت صفرش می‌خوانند، پس هر نام چاپگر دقیقاً با یک نویسه برمی‌گردد
// غلط: اندازه درست، نوع عنصر غلط. بدون خطای کامپایل، بدون crash،
// هر رشته بریده به یک نویسه
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... ده عضو دیگر
  end;

// درست: ساختار *W سرتاسر اعضای wide دارد
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... ده عضو دیگر
  end;

قاعده‌ای که از آن بیرون می‌آید مکانیکی است و ارزش اعمال بدون فکر کردن دارد: برای هر ساختار Win32 که نامش به W ختم می‌شود، بررسی کنید هر عضو رشته‌ای گونه wide است، فیلد به فیلد. مخلوط‌کردن دنیای ANSI و wide نه تشخیص کامپایلر تولید می‌کند نه crash، فقط برش بی‌صدا، و همین به‌طور معکوس برای گونه‌های ANSI هم صدق می‌کند

یک handler استثنای لخت حریف واقعی است

هر یک از این بررسی‌ها توسط همان سازه کند شد: handlerی که همه‌چیز را می‌گیرد و به یک مقدار بازگشت false تبدیل می‌کند. نوشتنش دور یک رمزگشای تصویر منطقی است، چون یک تصویر خراب نباید یک کار سند را بیندازد. اما همچنین وسیله‌ای است برای پاک‌کردن تک قطعه اطلاعاتی که به آن نیاز دارید

پاسخ عملی این است که handler را موقتاً پرصدا کنید. تخلیه کلاس استثنا، پیام و backtrace از درون handler لخت، زیر یک شرط debug، یک بازگشت null تبیین‌ناپذیر را به یک استثنای نام‌دار با یک مکان تبدیل می‌کند. در دو مورد از سه مورد بالا همان یک قدم بررسی را تمام کرد، چون استثنا یک تقسیم بر صفر یا یک access violation در متدی جانشین بود که نامش همه‌چیز را می‌گفت

چک‌لیست برای اتخاذ یک مسیر جانشین

چهار مورد، به ترتیبی که نتیجه می‌دهند. پیش از فراخوانی یک کلاس جایگزین، متدهایی را که می‌خواهید استفاده کنید بخوانید و مطمئن شوید هر یک بدنه واقعی دارد؛ یک بدنه خالی جزئیات پیاده‌سازی نیست، یک ویژگی غایب است. جانشین‌هایی که پرتاب می‌کنند را به جانشین‌هایی که مقدار خنثی برمی‌گردانند ترجیح دهید، و آن را با null check در جاهایی جفت کنید که کارخانه اکنون می‌تواند به‌طور مشروع هیچ‌چیز برگرداند. یک ویژگی را با بازرسی مصنوع راستی‌آزمایی کنید نه کد بازگشت، چون کل حالت شکست اینجا یک کد بازگشت تمیز روی یک مصنوع خالی است؛ تفکیک بایتی اینکه یک سند واقعاً چه دارد سریع‌ترین راه دیدن آن است، و مقاله ممیزی اندازه فایل آن ابزارها را پوشش می‌دهد. و وقتی یک ویژگی پیاده‌سازی جانشین قابل‌دوامی ندارد، نمونه‌های متأثر را به مسیری که کار می‌کند هدایت کنید و در یک کامنت بگویید چرا، به‌جای رهاکردن یک نمایش که بی‌سروصدا خروجی خالی تولید می‌کند

نکته کلی‌تر بسیار فراتر از یک کتابخانه صادق است. هر codebase با یک پیاده‌سازی دوم شرطی، یک لایه mock، یک حالت headless، یک shim پلتفرم، در معرض شکل سه است. دلیل اینکه این‌قدر خوب پنهان می‌شود این است که هر دروازه کیفیتی که یک تیم معمولاً به آن تکیه می‌کند، کدهای بازگشت، کدهای خطا، استثناها، وضعیت‌های خروج، یک مجرای وضعیت است، و شکل سه همه آن‌ها را تمیز نگه می‌دارد. فقط خروجی لو می‌دهد. همین استدلال پشت بررسی مصنوع‌ها به‌جای وضعیت‌ها هنگام کار با ورودی نامطمئن است، که در مقاله تجزیه PDF نامطمئن توصیف شده، و پشت مقایسه خروجی رندر میان موتورها به‌جای اعتماد به یکی، که در رندر چندموتوره توصیف شده است

PDFlibPas یک کتابخانه PDF بومی Object Pascal برای Delphi، C++Builder و Free Pascal است، و پیکربندی non-VCL آن است که بیلدهای headless و میان-toolchainی را ممکن می‌کند؛ پوشش پیکربندی فعلی در صفحه محصول losLab PDF Developer Library فهرست شده است