기술 문서

Pascal PDF 라이브러리의 조용한 스텁 실패 진단

Delphi 라이브러리에 비주얼 프레임워크 없는 빌드 구성이 생길 때 버그가 사는 곳은 대체 클래스입니다. 플랫폼도, 컴파일러도 아니라 스탠드인(stand-in)입니다. PDFlibPas에는 VCL 없는 빌드를 위해 비트맵, 캔버스, 폰트, 메타파일, 프린터 동등물을 제공하는 그래픽스 레이어가 있고, 이를 Free Pascal로 포팅하면서 스탠드인이 가질 수 있는 모든 실패 양상이 드러났습니다. 이것들은 진단 비용으로 깔끔하게 정렬되며, 그 순서는 직관이 예상하는 것의 반대입니다

예외를 던지는 스탠드인은 찾기 값싸고, 예외가 메서드 이름을 대신 말해 줍니다. 빈 데이터를 돌려주는 스탠드인은 값이 비쌉니다. 실패가 원인에서 몇 겹 떨어진 곳에 나타나기 때문입니다. 성공을 돌려주는 스탠드인이 가장 나쁩니다. 반환 코드는 유효하고, 오류 코드는 0이며, 예외는 던져지지 않고, 무언가 잘못되었다는 유일한 증거가 출력된 바이트 안에 있기 때문입니다

Pascal PDF 라이브러리의 조용한 스텁 실패 세 형태를 진단 비용 순으로, 예외를 던지는 스텁부터 빈 출력 위의 성공까지 나열한 그림
예외를 던지는 스탠드인은 진단이 값싸고, 빈 데이터는 비싸며, 빈 산출물 위의 성공 반환은 가장 찾기 어렵습니다

세 번째 형태: 빈 XObject 위의 유효한 이미지 식별자

벡터 메타파일 변환기는 non-VCL 구성에서 빈 procedure 본문이었습니다. 그 위의 모든 것은 계속 동작했습니다. EMF 가져오기 진입점과 캔버스 캡처 진입점은 끝까지 실행되어 정당한 이미지 식별자를 돌려주었고, 호출자는 그것을 페이지에 얹었습니다. 파일에 들어간 것은 콘텐츠 길이가 0인 폼 XObject였습니다. 페이지는 하얗게 렌더링되었습니다

아무것도 문제를 보고하지 않았습니다. 여기에는 이 기능의 라이브러리 자체 데모 프로그램도 포함되는데, 빈 페이지를 그리고도 눈치채지 못했습니다. 확인할 실패 반환값이 없었습니다. 호출의 나열이 실제로 모두 성공했기 때문이며, 잘못된 것은 만들어진 스트림의 크기 하나뿐이었습니다. 이 부류의 결함을 진단한다는 것은 다른 질문을 던지는 일입니다. "호출이 실패했는가"가 아니라 "산출물이 그럴듯한가"입니다. 길이 0의 폼 XObject, 픽셀 0짜리 이미지, 콘텐츠 바이트 0짜리 페이지, 이것들이 결함을 잡아 내는 단정입니다

수정은 두 절반으로 이루어지고, 두 번째 절반이 잊기 쉽습니다. 첫째, 빈 구현이 예외를 던지게 해 실패가 최소한의 채널이라도 갖게 합니다. 둘째, 이미지 팩토리에서 그 예외를 null 결과로 바꾸고 이미지 식별자를 소비하는 두 곳에 null 검사를 더합니다. 그렇지 않으면 "깨끗한 실패"가 아무것도 아닌 것의 역참조로 곧장 액세스 위반이 되기 때문입니다. 예외를 던지는 스텁이 개선인 것은 호출자들이 이전에는 받을 수 없었던 실패를 받을 준비가 되어 있을 때만입니다

두 번째 형태: 빈 데이터, 크래시까지 세 겹

메타파일 캔버스 스탠드인은 자신의 물리적 치수를 채우지 않았습니다. 그 값은 페이지 지오메트리 계산의 나눗수로 들어가므로 계산이 0을 내고, 바운딩 박스 계산은 0으로 나누게 됩니다. bare 예외 핸들러가 이를 삼켰고, 이미지 팩토리는 null 결과를 돌려주었으며, 액세스 위반은 null이 쓰이는 페이지 트리에서 마침내 일어났습니다. 원인과 증상 사이에 세 겹, 그 한가운데서 증거를 지우는 예외 핸들러가 있습니다

PDFlibPas의 빈 메타파일 캔버스 스탠드인 실패 사슬이 0으로 나누기 세 겹 뒤의 액세스 위반에 이르는 과정
빈 치수가 지오메트리 계산을 0으로 나누게 하고, bare 핸들러가 예외를 지우며, null 식별자가 페이지 트리를 크래시시킵니다

같은 유닛에는 이 패턴의 사례가 두 건 더 있었습니다. 폰트 클래스는 빈 Assign 본문과 생성자 본문을 갖고 있었는데, 겉보기보다 중요합니다. 캔버스의 폰트 프로퍼티가 읽기 전용이라 대입만이 폰트를 전달하는 유일한 방법이므로, 빈 구현은 폰트 선택을 조용히 무력화하고 텍스트는 기본값 무엇이든지로 나오게 만듭니다. 그리고 인치당 픽셀 값이 0이라 폰트 메트릭으로 캔버스 크기를 정하는 모든 호출자가 0 곱하기 0 캔버스를 만들었고, 이는 빈 페이지와 성공 반환으로 이어졌습니다

// 스탠드인 유닛에서 찾아야 할 형태: 예외도 던지지 않고
// 아무것도 하지 않는 메서드. 둘 다 컴파일되고 둘 다
// 출력 없이 "success"를 만들어 냅니다
procedure TMetafileCanvasStandIn.Create(...);
begin
  // inherited 호출 없음, 필드 초기화 없음
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // 그리고 비트맵은 여전히 비어 있습니다
end;

첫 문자만 남기는 와이드 구조체

이것은 스탠드인 문제와 전혀 무관하지만, 증상이 원인에서 똑같이 멀리 있어 같은 목록에 들어갑니다. 프린터 열거 구조체가 문자열 멤버 열두 개 전부를 싱글바이트 문자 포인터 타입으로 선언되어 있었는데, 그것을 채우는 함수는 열거 API의 와이드 문자 변형입니다

포인터 크기는 동일하므로 구조체 레이아웃은 맞고 아무것도 크래시하지 않습니다. 대신 일어나는 일은, UTF-16 문자열을 싱글바이트 문자열로 읽으면 첫 0 바이트에서 멈춘다는 것입니다. ASCII 프린터 이름이라면 그것은 두 번째 문자의 상위 바이트입니다. 모든 프린터 이름이 정확히 한 문자로 돌아왔습니다. 하류에서는 이름 검증이 실패하고, 프린터 생성이 실패하고, 기계의 모든 실제 프린터에 대해 인쇄가 실패했으며, 그 증상 어느 것도 구조체 선언을 가리키지 않습니다

와이드 Win32 구조체를 PWideChar가 아니라 PAnsiChar 멤버로 선언한 뒤 UTF-16 프린터 이름이 한 문자로 잘리는 모습
싱글바이트 멤버는 UTF-16 이름을 첫 0 바이트까지만 읽으므로, 모든 프린터 이름이 정확히 한 문자로 돌아옵니다
// 틀림: 크기는 맞고 요소 타입이 틀림. 컴파일 오류도 크래시도 없이
// 모든 문자열이 한 문자로 잘립니다
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... 열 개 더
  end;

// 맞음: *W 구조체는 전체가 와이드 멤버입니다
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... 열 개 더
  end;

여기서 나오는 규칙은 기계적이며 생각 없이 적용할 가치가 있습니다. 이름이 W로 끝나는 Win32 구조체는 모든 문자열 멤버가 와이드 변형인지 필드 단위로 확인합니다. ANSI와 와이드 세계를 섞으면 컴파일러 진단도 크래시도 없이 조용한 잘림만 남고, ANSI 변형에는 그 반대로 똑같이 적용됩니다

진짜 적은 bare 예외 핸들러입니다

이 조사들은 모두 같은 구성물 하나에 늦어졌습니다. 모든 것을 잡아 false 반환값으로 바꾸는 핸들러입니다. 이미지 디코더 주위에 이것을 쓰는 것은 합리적입니다. 손상된 이미지가 문서 작업 전체를 끌어내려서는 안 되기 때문입니다. 그러나 이것은 여러분이 필요한 단 한 조각의 정보를 지우는 장치이기도 합니다

실무적 대응은 핸들러를 임시로 시끄럽게 만드는 것입니다. bare 핸들러 안에서 디버그 조건부 아래 예외 클래스, 메시지, 백트레이스를 덤프하면 설명 없는 null 반환은 위치가 있는 이름 붙은 예외로 바뀝니다. 위 세 사례 중 두 사례는 이 한 단계로 조사가 끝났습니다. 예외가 0으로 나누기였거나, 이름이 모든 것을 말해 주던 스탠드인 메서드의 액세스 위반이었기 때문입니다

스탠드인 경로를 채택할 때의 체크리스트

네 항목, 회수가 큰 순서대로입니다. 대체 클래스를 호출하기 전에 사용하려는 메서드들을 읽고 각각 진짜 본문을 갖는지 확인하십시오. 빈 본문은 구현 세부가 아니라 빠진 기능입니다. 중립 값을 돌려주는 스탠드인보다 예외를 던지는 스탠드인을 선호하고, 팩토리가 이제 정당하게 아무것도 돌려주지 않을 수 있는 곳에는 null 검사를 짝지으십시오. 기능은 반환 코드가 아니라 산출물을 들여다보며 검증하십시오. 이 글의 실패 양상 전체가 빈 산출물 위의 깨끗한 반환 코드이기 때문입니다. 문서가 실제로 무엇을 담는지의 바이트 수준 분해가 가장 빠른 확인 방법이고, 파일 크기 감사 문서가 그 도구를 다룹니다. 그리고 기능에 쓸 만한 대체 구현이 없을 때는 조용히 빈 출력을 내는 데모를 남겨 두는 대신, 영향을 받는 샘플을 동작하는 경로로 돌리고 주석에 이유를 적으십시오

더 넓은 요점은 한 라이브러리를 훨씬 넘어 적용됩니다. 조건부 두 번째 구현, 목(mock) 레이어, 헤드리스 모드, 플랫폼 심(shim)을 갖춘 모든 코드베이스는 세 번째 형태에 노출되어 있습니다. 이 형태가 그렇게 잘 숨는 이유는 팀이 평소 의지하는 모든 품질 게이트, 즉 반환 코드, 오류 코드, 예외, 종료 상태가 모두 상태 채널이고, 세 번째 형태는 그 모두를 깨끗하게 유지하기 때문입니다. 배신하는 것은 출력뿐입니다. 이는 신뢰할 수 없는 입력을 다룰 때 상태가 아니라 산출물을 확인하는 근거이기도 하며, 신뢰할 수 없는 PDF 파싱 문서에서 설명합니다. 또한 하나를 믿는 대신 여러 엔진의 렌더링 출력을 비교하는 근거이기도 하고, 멀티 엔진 렌더링에서 설명합니다

PDFlibPas는 Delphi, C++Builder, Free Pascal용 네이티브 Object Pascal PDF 라이브러리이며, non-VCL 구성이 헤드리스와 크로스 툴체인 빌드를 가능하게 하는 바로 그 구성입니다. 현재 구성 커버리지는 losLab PDF Developer Library 제품 페이지에 정리되어 있습니다