When a Delphi library grows a build configuration without the visual framework, the substitute classes are where the bugs live. Not the platform, not the compiler: the stand-ins. PDFlibPas has a graphics layer that provides bitmap, canvas, font, metafile and printer equivalents for builds without the VCL, and porting it to Free Pascal surfaced every failure mode a stand-in can have. They sort neatly by diagnosis cost, and the ordering is the opposite of what intuition suggests
A stand-in that raises is cheap to find; the exception names the method. A stand-in that returns empty data is expensive, because the failure appears several layers away from its cause. A stand-in that returns success is the worst of all, because the return code is valid, the error code is zero, no exception is raised, and the only evidence anything went wrong is in the bytes that came out
Shape three: a valid image identifier over an empty XObject
The vector metafile converter was an empty procedure body in the non-VCL configuration. Everything above it kept working. The EMF import entry points and the canvas-capture entry point ran to completion and returned a legal image identifier, which the caller then placed on a page. What landed in the file was a form XObject with a content length of zero. The page rendered white
Nothing reported a problem, and that includes the library's own demonstration program for this feature, which drew a blank page and did not notice. There was no failing return value to check, because the sequence of calls genuinely all succeeded; the only thing that was wrong was the size of the produced stream. Diagnosing this class of defect means asking a different question: not "did the call fail" but "is the artifact plausible". A form XObject of length zero, an image of zero pixels, a page of zero content bytes, these are the assertions that catch it
The fix has two halves and the second half is easy to forget. First, make the empty implementation raise, so the failure has a channel at all. Second, convert that exception into a null result at the image factory and add null checks at the two places that consume an image identifier, because otherwise "clean failure" turns straight into an access violation as the page tree dereferences nothing. A stub that throws is only an improvement if the callers were prepared for a failure they had never previously been able to receive
Shape two: empty data, three layers from the crash
The metafile canvas stand-in did not fill in its physical dimensions. That value divides into a page geometry calculation, so the calculation produced zero, so the bounding-box computation divided by zero. A bare exception handler swallowed that, the image factory returned a null result, and the access violation finally happened in the page tree when the null was used. Three layers between cause and symptom, with an exception handler in the middle erasing the evidence
The same unit had two more instances of the pattern. The font class had empty Assign and constructor bodies, which matters more than it looks because the canvas font property is read-only: assigning into it is the only way to deliver a font, so an empty implementation makes font selection silently ineffective and the text comes out in whatever the default was. And a pixels-per-inch value of zero made every caller that sizes a canvas from font metrics produce a zero-by-zero canvas, which yields a blank page and a success return
// The shape to look for in a stand-in unit: a method that neither
// raises nor does anything. Both of these compile and both of them
// produce "success" with no output
procedure TMetafileCanvasStandIn.Create(...);
begin
// no inherited call, no field initialisation
end;
function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
Result := True; // and the bitmap is still empty
end;
The wide structure that keeps only the first character
This one is not a stand-in problem at all, but it belongs in the same catalog because the symptom is equally far from the cause. The printer enumeration structure was declared with all twelve of its string members typed as pointers to single-byte characters, while the function that fills it is the wide-character variant of the enumeration API
Pointer sizes are identical, so the structure layout is correct and nothing crashes. What happens instead is that reading a UTF-16 string as a single-byte string stops at the first zero byte, which for any ASCII printer name is the high half of the second character. Every printer name came back as exactly one character. Downstream, name validation failed, printer creation failed and printing failed for every real printer on the machine, and none of those symptoms points at a structure declaration
// Wrong: right size, wrong element type. No compile error, no crash,
// every string truncated to one character
type
TPrinterInfo2Wrong = record
pServerName: PAnsiChar;
pPrinterName: PAnsiChar;
// ... ten more
end;
// Right: a *W structure has wide members throughout
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... ten more
end;
The rule that comes out of it is mechanical and worth applying without thinking: for any Win32 structure whose name ends in W, verify every string member is the wide variant, field by field. Mixing the ANSI and wide worlds produces neither a compiler diagnostic nor a crash, only silent truncation, and the same applies in reverse to the ANSI variants
A bare exception handler is the actual adversary
Every one of these investigations was slowed by the same construct: a handler that catches everything and converts it to a false return value. It is a reasonable thing to write around an image decoder, since a corrupt image should not take down a document job. It is also a device for deleting the one piece of information you need
The practical response is to make the handler temporarily loud. Dumping the exception class, message and backtrace from inside the bare handler, under a debug conditional, converts an unexplained null return into a named exception with a location. In two of the three cases above that single step ended the investigation, because the exception was a division by zero or an access violation in a stand-in method whose name said everything
Checklist for adopting a stand-in path
Four items, in the order they pay off. Before calling into a substitute class, read the methods you are about to use and confirm each has a real body; an empty body is not an implementation detail, it is a missing feature. Prefer stand-ins that raise over stand-ins that return neutral values, and pair that with null checks at the places a factory can now legitimately return nothing. Verify a feature by inspecting the artifact, not the return code, since the whole failure mode here is a clean return code over an empty artifact; a byte-level breakdown of what a document actually contains is the fastest way to see it, and the file size audit article covers that tooling. And when a feature has no viable substitute implementation, route the affected samples to the path that does work and say why in a comment, rather than leaving a demonstration that quietly produces blank output
The broader point applies well beyond one library. Any codebase with a conditional second implementation, a mock layer, a headless mode, a platform shim, is exposed to shape three. The reason it hides so well is that every quality gate a team normally relies on, return codes, error codes, exceptions, exit statuses, is a status channel, and shape three keeps all of them clean. Only the output betrays it. That is also the reasoning behind checking artifacts rather than statuses when handling untrusted input, described in the untrusted PDF parsing article, and behind comparing rendered output across engines rather than trusting one, described in multi-engine rendering
PDFlibPas is a native Object Pascal PDF library for Delphi, C++Builder and Free Pascal, and its non-VCL configuration is what makes headless and cross-toolchain builds possible; current configuration coverage is listed on the losLab PDF Developer Library product page