Когда библиотека Delphi обрастает конфигурацией сборки без визуального каркаса, классы-заменители — то место, где живут ошибки. Не платформа, не компилятор: подмены. PDFlibPas имеет графический слой, дающий эквиваленты растрового изображения, канвы, шрифта, метафайла и принтера для сборок без VCL, и перенос на Free Pascal высветил все режимы отказа, какие подмена может иметь. Они аккуратно сортируются по стоимости диагностики, и порядок противоположен тому, что подсказывает интуиция
Подмена, которая возбуждает исключение, дешева в поиске: исключение называет метод. Подмена, возвращающая пустые данные, дорога, поскольку отказ проявляется в нескольких слоях от своей причины. Подмена, возвращающая успех, хуже всех: код возврата корректен, код ошибки равен нулю, исключение не возбуждается, и единственное свидетельство неполадки — в вышедших байтах
Форма три: корректный идентификатор изображения над пустым XObject
Векторный конвертер метафайлов в конфигурации без VCL был пустым телом процедуры. Всё над ним продолжало работать. Точки входа импорта EMF и точка захвата канвы выполнялись до конца и возвращали легальный идентификатор изображения, который вызывающий затем размещал на странице. В файл попадал form XObject с длиной содержимого ноль. Страница отрисовывалась белой
Ничего не сообщало о проблеме, включая собственную демонстрационную программу библиотеки для этой возможности, которая рисовала пустую страницу и не замечала этого. Не было неудачного возвращаемого значения, которое можно проверить, поскольку вся последовательность вызовов действительно завершилась успешно; неверно было только одно — размер получившегося потока. Диагностика этого класса дефектов означает другой вопрос: не «потерпел ли вызов неудачу», а «правдоподобен ли артефакт». Form XObject нулевой длины, изображение в ноль пикселей, страница в ноль байтов содержимого — вот утверждения, которые это ловят
Исправление состоит из двух половин, и вторую легко забыть. Во-первых, заставьте пустую реализацию возбуждать исключение, чтобы у отказа вообще появился канал. Во-вторых, преобразуйте это исключение в нулевой результат на фабрике изображений и добавьте проверки на ноль в двух местах, потребляющих идентификатор изображения, иначе «чистый отказ» превратится прямо в нарушение доступа, когда дерево страниц разыменует пустоту. Заглушка с исключением — улучшение, только если вызывающие были готовы к отказу, который раньше получить не могли
Форма два: пустые данные в трёх слоях от падения
Подмена канвы метафайла не заполняла свои физические размеры. Это значение делится в вычислении геометрии страницы, поэтому вычисление давало ноль, а вычисление ограничивающего прямоугольника делило на ноль. Голый обработчик исключений это проглотил, фабрика изображений вернула нулевой результат, и нарушение доступа в конце концов случилось в дереве страниц, когда ноль использовали. Три слоя между причиной и симптомом, а посередине обработчик исключений стирает улики
В том же модуле было ещё два экземпляра этого образца. Класс шрифта имел пустые тела Assign и конструктора, что важнее, чем кажется: свойство шрифта канвы только для чтения, и присваивание в него — единственный способ передать шрифт, поэтому пустая реализация делает выбор шрифта молча неэффективным, и текст выходит тем, чем был по умолчанию. А значение пикселей на дюйм, равное нулю, заставляло каждого вызывающего, размечающего канву по метрикам шрифта, создавать канву ноль на ноль, что даёт пустую страницу и успешный код возврата
// Образец, который стоит искать в модуле подмен: метод, который не
// возбуждает исключение и ничего не делает. Оба фрагмента компилируются
// и оба выдают «успех» без результата
procedure TMetafileCanvasStandIn.Create(...);
begin
// ни вызова inherited, ни инициализации полей
end;
function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
Result := True; // а растровое изображение всё ещё пусто
end;
Широкая структура, сохраняющая только первый символ
Это вовсе не проблема подмен, но попадает в тот же каталог, потому что симптом столь же далёк от причины. Структура перечисления принтеров была объявлена со всеми двенадцатью строковыми членами как указателями на однобайтовые символы, тогда как заполняющая её функция — вариант перечисления API для широких символов
Размеры указателей идентичны, поэтому раскладка структуры верна и ничего не падает. Вместо этого чтение строки UTF-16 как однобайтовой останавливается на первом нулевом байте, которым для любого имени принтера в ASCII является старшая половина второго символа. Каждое имя принтера возвращалось ровно из одного символа. Ниже по течению не проходила проверка имени, не создавался принтер и не печатала ни одна настоящая печатная машина компьютера, и ни один из этих симптомов не указывает на объявление структуры
// Неверно: верный размер, неверный тип элемента. Ни ошибки компиляции,
// ни падения, каждая строка усечена до одного символа
type
TPrinterInfo2Wrong = record
pServerName: PAnsiChar;
pPrinterName: PAnsiChar;
// ... ещё десять
end;
// Верно: структура *W имеет широкие члены повсюду
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... ещё десять
end;
Правило, вытекающее из этого, механическое, и применять его стоит не думая: для любой структуры Win32, чьё имя кончается на W, проверяйте каждый строковый член как широкий вариант, поле за полем. Смешение миров ANSI и широких символов не даёт ни диагностики компилятора, ни падения — только молчаливое усечение, и то же в обратную сторону верно для ANSI-вариантов
Голый обработчик исключений — настоящий противник
Каждое из этих расследований тормозила одна и та же конструкция: обработчик, ловящий всё и превращающий в ложное возвращаемое значение. Вокруг декодера изображений это разумно написать: испорченное изображение не должно валить задание документа. Но это также приспособление для удаления единственной нужной вам информации
Практический ответ — временно сделать обработчик громким. Выгрузка класса исключения, сообщения и трассировки изнутри голого обработчика под отладочной условной директивой превращает необъяснимый нулевой возврат в именованное исключение с местом. В двух из трёх случаев выше этот единственный шаг завершил расследование, ведь исключением было деление на ноль или нарушение доступа в методе подмены, чьё имя говорило всё
Контрольный список для перехода на путь подмен
Четыре пункта в порядке отдачи. Прежде чем вызывать класс-заменитель, прочтите методы, которые собираетесь использовать, и убедитесь, что у каждого настоящее тело; пустое тело — не деталь реализации, а отсутствующая возможность. Предпочитайте подмены, возбуждающие исключения, подменам, возвращающим нейтральные значения, и дополните это проверками на ноль в тех местах, где фабрика теперь может легитимно не вернуть ничего. Проверяйте возможность осмотром артефакта, а не кода возврата, ведь весь этот режим отказа — чистый код возврата над пустым артефактом; побайтовая разбивка того, что документ действительно содержит, — быстрейший способ это увидеть, и статья об аудите размера файла описывает такой инструмент. А когда у возможности нет жизнеспособной заменяющей реализации, направьте затронутые образцы на работающий путь и объясните почему в комментарии, вместо того чтобы оставлять демонстрацию, тихо выдающую пустой результат
Более общий вывод применим далеко за пределами одной библиотеки. Любая кодовая база с условной второй реализацией, слоем имитаций, безголовым режимом, платформенной прокладкой подвергнута форме три. Причина, по которой она так хорошо прячется, в том, что каждый заслон качества, на который команда обычно опирается, — коды возврата, коды ошибок, исключения, статусы выхода, — является каналом статуса, а форма три держит все их чистыми. Предаёт её только вывод. На том же рассуждении построена проверка артефактов, а не статусов при обработке недоверенного ввода, описанная в статье о разборе недоверенных PDF, и сравнение отрисованного вывода между движками вместо доверия одному, описанное в мультидвижковой отрисовке
PDFlibPas — собственная PDF-библиотека на Object Pascal для Delphi, C++Builder и Free Pascal, и именно её конфигурация без VCL делает возможными безголовые и кросс-цепочные сборки; текущее покрытие конфигураций перечислено на странице продукта losLab PDF Developer Library