Bài viết kỹ thuật

Chẩn đoán thất bại stub câm lặng trong thư viện PDF Pascal

Khi một thư viện Delphi mọc thêm một cấu hình build không có framework giao diện, các lớp thay thế chính là nơi bug cư trú. Không phải nền tảng, không phải trình biên dịch: mà là các stand-in. PDFlibPas có một tầng đồ họa cung cấp các đối tượng tương đương bitmap, canvas, phông, metafile và máy in cho các bản build không có VCL, và việc chuyển nền nó sang Free Pascal đã làm lộ ra mọi dạng thất bại mà một stand-in có thể có. Chúng xếp gọn gàng theo chi phí chẩn đoán, và thứ tự đó ngược với điều trực giác gợi ý

Một stand-in ném ngoại lệ thì rẻ để tìm; ngoại lệ nêu tên phương thức. Một stand-in trả về dữ liệu rỗng thì đắt, vì thất bại xuất hiện cách nguyên nhân vài tầng. Một stand-in trả về thành công là tệ nhất trong các trường hợp, vì mã trả về hợp lệ, mã lỗi bằng không, không ngoại lệ nào được ném, và bằng chứng duy nhất rằng có chuyện gì đó sai nằm trong những byte đã được tạo ra

Ba dạng thất bại stub câm lặng trong thư viện PDF Pascal xếp theo chi phí chẩn đoán, từ stub ném ngoại lệ đến thành công trên đầu ra rỗng
Stand-in ném ngoại lệ rẻ để chẩn đoán, dữ liệu rỗng thì đắt, còn mã trả về thành công trên một sản phẩm rỗng là tệ nhất để tìm

Dạng ba: một định danh ảnh hợp lệ trên một XObject rỗng

Bộ chuyển đổi metafile vector là một thân thủ tục rỗng trong cấu hình không-VCL. Mọi thứ phía trên nó vẫn tiếp tục chạy. Các điểm vào nhập EMF và điểm vào bắt canvas chạy đến hoàn tất và trả về một định danh ảnh hợp pháp, mà caller sau đó đặt lên một trang. Thứ nằm trong tệp là một form XObject với chiều dài nội dung bằng không. Trang kết xuất ra trắng tinh

Không gì báo có vấn đề, và điều đó bao gồm cả chương trình demo của chính thư viện cho tính năng này, thứ đã vẽ một trang trắng mà không hay biết. Không có giá trị trả về thất bại nào để kiểm tra, vì chuỗi lệnh gọi thực sự đều thành công; thứ duy nhất sai là kích thước của luồng được tạo ra. Chẩn đoán lớp khuyết tật này nghĩa là hỏi một câu khác: không phải "lệnh gọi có thất bại không" mà là "sản phẩm có hợp lý không". Một form XObject độ dài không, một ảnh không pixel, một trang không byte nội dung, đó là những khẳng định bắt được nó

Bản sửa có hai nửa và nửa thứ hai dễ bị quên. Một, làm cho hiện thực rỗng ném ngoại lệ, để thất bại có kênh truyền đạt. Hai, chuyển ngoại lệ đó thành kết quả null tại nhà máy ảnh và thêm kiểm tra null tại hai nơi tiêu thụ định danh ảnh, vì nếu không "thất bại sạch sẽ" sẽ biến thẳng thành access violation khi cây trang hủy tham chiếu vào hư vô. Một stub ném ngoại lệ chỉ là cải tiến nếu các caller đã chuẩn bị cho một thất bại mà trước đây chúng chưa từng có thể nhận được

Dạng hai: dữ liệu rỗng, cách crash ba tầng

Stand-in canvas metafile không điền các kích thước vật lý của nó. Giá trị đó chia vào một phép tính hình học trang, nên phép tính ra không, nên phép tính bounding-box chia cho không. Một trình xử lý ngoại lệ trần nuốt chửng nó, nhà máy ảnh trả về kết quả null, và access violation cuối cùng xảy ra trong cây trang khi null được dùng. Ba tầng giữa nguyên nhân và triệu chứng, với một trình xử lý ngoại lệ ở giữa xóa bằng chứng

Chuỗi thất bại của stand-in canvas metafile rỗng trong PDFlibPas đi đến access violation ba tầng sau phép chia cho không
Kích thước rỗng kéo một phép tính hình học về không, trình xử lý trần xóa ngoại lệ, và định danh null làm cây trang sập

Cùng unit đó còn có thêm hai trường hợp của mô hình này. Lớp phông có các thân Assign và constructor rỗng, điều này quan trọng hơn vẻ ngoài của nó vì thuộc tính phông của canvas là chỉ đọc: gán vào đó là cách duy nhất để chuyển giao một phông, nên một hiện thực rỗng khiến việc chọn phông lặng lẽ vô hiệu và văn bản ra với phông mặc định tùy hứng. Và một giá trị pixel-mỗi-inch bằng không khiến mọi caller định kích thước canvas từ các số đo phông tạo ra một canvas không-nhân-không, cho ra một trang trắng và mã trả về thành công

// Dạng cần tìm trong một unit stand-in: một phương thức chẳng ném
// ngoại lệ cũng chẳng làm gì. Cả hai đoạn này đều biên dịch được và
// đều tạo ra "thành công" mà không có đầu ra
procedure TMetafileCanvasStandIn.Create(...);
begin
  // không gọi inherited, không khởi tạo trường
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // và bitmap vẫn rỗng
end;

Cấu trúc wide chỉ giữ lại đúng ký tự đầu tiên

Trường hợp này chẳng phải vấn đề stand-in chút nào, nhưng nó thuộc cùng danh mục vì triệu chứng xa nguyên nhân chẳng kém. Cấu trúc liệt kê máy in được khai báo với cả mười hai thành viên chuỗi của nó có kiểu là con trỏ tới ký tự một byte, trong khi hàm điền nó là biến thể wide-character của API liệt kê

Kích thước con trỏ là giống hệt nhau, nên bố cục cấu trúc đúng và chẳng gì sập. Điều xảy ra thay vào đó là việc đọc một chuỗi UTF-16 như một chuỗi một byte dừng ở byte không đầu tiên, vốn với bất kỳ tên máy in ASCII nào là nửa cao của ký tự thứ hai. Mọi tên máy in trả về đúng một ký tự. Phía hạ nguồn, xác thực tên thất bại, tạo máy in thất bại và in ấn thất bại với mọi máy in thật trên máy, và chẳng triệu chứng nào trong số đó chỉ về một khai báo cấu trúc

Tên máy in UTF-16 bị cắt còn một ký tự sau khi một cấu trúc Win32 wide được khai báo với thành viên PAnsiChar thay vì PWideChar
Thành viên một byte đọc tên UTF-16 chỉ đến byte không đầu tiên, nên mọi tên máy in trả về với đúng một ký tự
// Sai: đúng kích thước, sai kiểu phần tử. Không lỗi biên dịch, không
// crash, mọi chuỗi bị cắt còn đúng một ký tự
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... còn mười nữa
  end;

// Đúng: cấu trúc *W có thành viên wide xuyên suốt
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... còn mười nữa
  end;

Quy tắc rút ra từ đó là cơ học và đáng áp dụng mà không cần suy nghĩ: với bất kỳ cấu trúc Win32 nào có tên kết thúc bằng W, kiểm tra từng trường một rằng mọi thành viên chuỗi đều là biến thể wide. Trộn thế giới ANSI và wide không tạo ra chẩn đoán trình biên dịch nào cũng như không crash, chỉ có cắt cụt âm thầm, và điều tương tự áp dụng ngược lại cho các biến thể ANSI

Trình xử lý ngoại lệ trần mới là kẻ thù thực sự

Mỗi cuộc điều tra trong số này đều bị chậm lại bởi cùng một cấu trúc: một handler bắt mọi thứ và chuyển nó thành giá trị trả về false. Đó là điều hợp lý để viết quanh một bộ giải mã ảnh, vì một ảnh hỏng không nên hạ gục cả một tác vụ tài liệu. Nó cũng là một công cụ xóa đi đúng mảnh thông tin bạn cần

Phản ứng thực tiễn là làm cho handler tạm thời ồn ào. Kết xuất class ngoại lệ, thông điệp và backtrace từ bên trong handler trần, dưới một conditional debug, biến một mã trả về null không giải thích được thành một ngoại lệ có tên kèm vị trí. Trong hai trên ba trường hợp ở trên, một bước duy nhất đó đã chấm dứt cuộc điều tra, vì ngoại lệ là một phép chia cho không hoặc một access violation trong một phương thức stand-in mà tên của nó nói lên tất cả

Danh mục kiểm tra khi áp dụng một con đường stand-in

Bốn mục, theo thứ tự chúng có lời. Trước khi gọi vào một lớp thay thế, hãy đọc các phương thức bạn sắp dùng và xác nhận mỗi phương thức có một thân thật; một thân rỗng không phải chi tiết hiện thực, nó là một tính năng còn thiếu. Ưu tiên stand-in ném ngoại lệ hơn stand-in trả về giá trị trung tính, và đi kèm với kiểm tra null tại những nơi một nhà máy giờ đây hợp pháp có thể trả về hư vô. Xác minh một tính năng bằng cách xem xét sản phẩm, không phải mã trả về, vì toàn bộ dạng thất bại ở đây là một mã trả về sạch trên một sản phẩm rỗng; một bản phân tích cấp byte về tài liệu thực sự chứa gì là cách nhanh nhất để thấy điều đó, và bài viết về kiểm toán kích thước tệp đề cập công cụ đó. Và khi một tính năng không có hiện thực thay thế khả thi, hãy đưa các mẫu bị ảnh hưởng sang con đường hoạt động được và nói rõ lý do trong một chú thích, thay vì để lại một bản trình diễn lặng lẽ tạo ra đầu ra trắng

Luận điểm rộng hơn áp dụng xa hơn một thư viện. Bất kỳ codebase nào có một hiện thực thứ hai theo conditional, một tầng mock, một chế độ headless, một platform shim, đều dễ dính dạng ba. Lý do nó giấu giếm giỏi đến thế là mọi cổng chất lượng mà một đội ngũ thường dựa vào — mã trả về, mã lỗi, ngoại lệ, trạng thái thoát — đều là kênh trạng thái, và dạng ba giữ cho tất cả chúng sạch bong. Chỉ có đầu ra là phản bội nó. Đó cũng là lý lẽ đằng sau việc kiểm tra sản phẩm thay vì trạng thái khi xử lý đầu vào không tin cậy, được mô tả trong bài viết về phân tích PDF không tin cậy, và đằng sau việc so sánh đầu ra kết xuất giữa các engine thay vì tin một engine, được mô tả trong kết xuất đa engine

PDFlibPas là một thư viện PDF Object Pascal thuần cho Delphi, C++Builder và Free Pascal, và cấu hình không-VCL của nó là thứ làm cho các bản build headless và đa toolchain khả thi; mức phủ cấu hình hiện tại được liệt kê trên trang sản phẩm losLab PDF Developer Library