PDFlibPas 3.538.0 ลิงก์ตัวเข้ารหัส JBIG2 ภายนอกเข้าไปแบบสแตติกในโปรแกรม Free Pascal และ Lazarus โปรเจกต์จะเพิ่มยูนิต PDFlibJBIG2EncC ซึ่งเป็นยูนิตเดียวกับที่ Delphi และ C++Builder ใช้อยู่แล้ว และตัวเข้ารหัสจะอยู่ภายในไฟล์ executable โดยไม่ต้องแจกจ่ายไฟล์อื่นเพิ่ม นี่ทำให้ข้อสรุปเดิมของฟีเจอร์นี้ต้องเปลี่ยนไป เพราะก่อนหน้านี้เข้าใจกันว่า Free Pascal เรียกใช้ตัวเข้ารหัสภายนอกได้ผ่าน DLL เท่านั้น
ทำไม DLL จึงดูเหมือนเป็นทางเลือกเดียว
DLL ดูเหมือนเป็นทางเลือกเดียวเพราะเส้นทางการลิงก์สามแบบล้มเหลวด้วยเหตุผลที่ไม่เกี่ยวกัน และไม่มี compiler switch ใดแตะปัญหาเหล่านั้นได้ linker ภายในปฏิเสธ associative COMDAT sections โดยตรง การลิงก์ภายนอกผ่าน binutils ที่ให้มาล่มระหว่างการเก็บ section ที่ไม่ถูกใช้ เพราะ Free Pascal ส่งขั้นตอนนี้เข้ามาโดยไม่มีเงื่อนไขบนเป้าหมาย Windows 64 บิต ส่วน binutils รุ่นใหม่กว่ากลับประมวลผล link script ของ Free Pascal ไม่ได้เลย การ rebuild ฝั่ง C++ ด้วย toolchain อีกชุดจึงเพียงเปลี่ยนจากการปฏิเสธแบบหนึ่งไปเป็นอีกแบบหนึ่ง เพราะ template และ inline instantiation สร้าง weak external symbols โดยธรรมชาติ และ Free Pascal รายงานสิ่งเหล่านั้นเป็น Unsupported COFF symbol type 105 หลักฐานทั้งหมดนี้ไม่ได้ผิด และ คำอธิบายก่อนหน้าเกี่ยวกับ backend ของตัวเข้ารหัส JBIG2 และ Free Pascal linker ก็ไล่ผ่านทางตันเหล่านี้ในรูปแบบที่ยังทำซ้ำได้จนถึงวันนี้ สิ่งที่ผิดคือสมมติฐานว่าเราจะแก้ปัญหาได้ที่จุดใด ทุกความพยายามผ่าน compiler หรือ linker แต่ทั้งคู่เปลี่ยนสิ่งที่อยู่ใน object file แล้วไม่ได้ ปัญหาอยู่ใน object file มาตลอด ObjConv อ่าน COFF และเขียน COFF กลับ และทุก construct ที่ Free Pascal รับไม่ไหวมีรูปแบบเทียบเท่าเชิงกลที่มันยอมรับ
ข้อผิดพลาดที่ไม่เคยบอกสาเหตุ
Free Pascal internal linker รองรับ pick-any COMDAT เพียงครึ่งเดียว และ implementation ครึ่ง ๆ กลาง ๆ นี้คือส่วนที่วินิจฉัยยากที่สุด มันพับนิยามที่ซ้ำกันได้ตามที่ format กำหนด แต่เมื่อ TExeOutput.RemoveUnreferencedSections ทำเครื่องหมาย section ว่าถูกใช้งาน มันจะ redirect ผ่าน exesymbol ไปยังนิยามที่ชนะ ขณะที่ TCoffexeoutput.DoRelocationFixup กลับอ่าน objreloc.symbol.objsection โดยตรง เมื่อ section ที่ถูกใช้งานอ้างถึง symbol ซึ่ง object ของตัวเองนิยามไว้ในสำเนาที่แพ้การ fold ทั้งสอง pass จึงมองเห็นคนละ section และการลิงก์จะหยุดที่ Internal error 200603061
ลองเทียบกับข้อจำกัดสองแบบที่อยู่ข้างเคียง Unsupported COFF symbol type 105 บอกว่าเป็น weak external ส่วน Associative or exact match COMDAT sections are not yet supported บอกว่าเป็น associative COMDAT และยังระบุ symbol ที่มีปัญหาด้วย แต่ Internal error 200603061 ไม่บอกอะไรเลย ไม่มีชื่อ symbol ไม่มีชื่อ section ไม่มีชื่อไฟล์ และไม่มีข้อมูลว่าเกิดใน phase ใด มันยังเป็นกรณีปกติ ไม่ใช่กรณีขอบ เพราะ MSVC ใส่ string literal ทุกตัว รวมถึง inline หรือ template instantiation ทุกตัว ลงใน pick-any COMDAT และใน object 186 ตัวของชุด encoder นี้ linker ทำการ fold ถึง 2656 ครั้ง การ build ด้วย /Gy- ช่วยให้ฟังก์ชันทั่วไปไม่อยู่ใน per-function COMDAT sections แต่ string literal และ template instantiation ยังอยู่ที่เดิม
ทำไมการเติม stub ให้ CRT ทีละ symbol จึงดูเหมือนตัวสุดท้ายทำให้พัง
เพราะ linker จะเข้าสู่ fixup pass ได้ก็ต่อเมื่อ resolve symbol ครบทั้งหมดเท่านั้น ระหว่างที่ยังขาด symbol ใดอยู่ การทำงานจะจบก่อนด้วย Undefined symbol และปัญหา COMDAT จะยังไม่มีโอกาสโผล่ขึ้นมา เมื่อเติม C runtime stub ตัวสุดท้าย linker จึงขยับไปอีก phase และชนกับ Internal error 200603061 ทันที อาการที่เห็นจึงชวนให้เข้าใจผิดอย่างเป็นระบบ: เมื่อเพิ่ม Pascal body ให้ C symbol ที่ถูกอ้างถึงทีละตัว จะดูเหมือนว่าตัวล่าสุดทำให้ build พัง หรือเหมือนข้าม threshold ราวหนึ่งร้อย stub ไปแล้ว ทั้งสองอย่างไม่จริงว่า symbol ไหนถูกเติมเป็นตัวสุดท้ายหรือเติมทั้งหมดกี่ตัวไม่สำคัญ เพราะ failure แฝงอยู่ตั้งแต่ object ตัวแรก และเพิ่งเข้าถึงได้เมื่อ resolution สำเร็จ เมื่อ linker เปลี่ยนข้อความร้องเรียนหลังจากคุณแก้ปัญหาที่ไม่เกี่ยวกัน ให้ถามก่อนว่าคุณเพียงเลื่อน phase ไปข้างหน้า หรือสร้าง regression ขึ้นมาจริง
วิธีแก้คือรัน ObjConv หนึ่งรอบ ไม่ใช่ compiler flag
การแก้ทั้งหมดคือคำสั่ง post-processing เดียวที่รันกับ object ที่ compile แล้วแต่ละตัว: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_ ในงานนี้มีการเพิ่ม option สามตัว -xw แปลง symbol IMAGE_SYM_CLASS_WEAK_EXTERNAL ให้เป็น external ปกติ -xn ทำให้ symbol IMAGE_SYM_CLASS_NULL เช่น _fltused อยู่ในรูปมาตรฐาน ไม่เช่นนั้น Free Pascal จะรายงานเป็น Unsupported COFF symbol type 0 ส่วน -xc คือหัวใจของการแก้ มันลดระดับ COMDAT section ทุกตัวให้เป็น plain section และทำให้ symbol ที่นิยามในนั้นเป็น static การล้มเหลวหายไปเพราะ decision ที่ทำให้เกิดปัญหาถูกลบออกไปด้วย เมื่อไม่มี COMDAT section ก็ไม่มีการ fold ไม่มีสำเนาที่ชนะให้ pass หนึ่ง redirect ไปหาแต่ pass อื่นหาไม่เจอ และ associative .pdata กับ .xdata unwind section ก็หายไปพร้อมกัน ต้นทุนมีจริงแต่เล็ก สำเนาที่เดิม merge ได้อย่างถูกต้องจะถูกเก็บไว้แยกกันทุกสำเนา
การเปลี่ยนชื่อ prefix -np:__imp_:pdflibimp_ แก้ collision อีกเรื่องหนึ่ง MSVC เรียก Win32 API ที่ import ผ่าน indirection cell ชื่อ __imp_* ขณะที่ Free Pascal สงวน prefix นี้ไว้กับ import machinery ของตัวเอง และการนิยามชื่อใดชื่อหนึ่งตรง ๆ จะทำให้เกิด Internal error 200603061 แบบเดิม การเปลี่ยนชื่อ cell ทำให้ฝั่ง Pascal ประกาศเป็นตัวแปรทั่วไปและเติมค่าใน runtime ได้ ส่วน object เอง compile ด้วย static-link flags /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- และปิด image codec ทำให้เส้นทาง file I/O และ codec ที่ไม่ถูกใช้ต้องการ stub สำหรับ link เท่านั้นน้อยลง ไฟล์เหล่านี้อยู่ใน Lib\thirdparty\Win64f ขณะที่เส้นทาง Delphi และ C++Builder ยังคงลิงก์ชุด Win64x ของตัวเองโดยไม่เปลี่ยน ซึ่งเป็นผลลัพธ์ที่ถูกต้องสำหรับ portability fix ที่จำกัดอยู่กับ toolchain เดียว
ฝั่ง Pascal ยังต้อง export อะไรบ้าง
Free Pascal resolve import ของ C object ด้วยชื่อ symbol และต้องระบุชื่อนั้นให้ชัด ดังนั้น Pascal routine ทุกตัวที่ทำหน้าที่แทน C entry point ต้องมี clause public name อย่างชัดเจน Delphi ใช้ชื่อ routine เป็นชื่อ symbol และไม่ต้องมี clause เลย นี่จึงเป็นเหตุผลที่ยูนิตเดียวรองรับทั้งสอง compiler โดยวาง clause ไว้ใต้ {$IFDEF FPC} จุดที่มักพลาดคือ declaration แบบ external 'msvcrt.dll' ไม่ได้ตอบโจทย์ linker มันสร้าง import แต่ไม่เคยสร้าง definition ที่ linked object จะ bind ได้ ต้องมี forwarding body อยู่จริง
// declaration ภายนอกสร้างได้แค่ import ไม่มี linked object ใด
// bind เข้าหามันได้
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// ต้องเผยแพร่ Pascal body ภายใต้ชื่อ C symbol ที่ตรงกัน
// object set จึงจะ bind เข้าหาได้จริง
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Variadic entry point ทำให้ pattern นี้ใช้ไม่ได้ เพราะ Pascal wrapper forward varargs ของตัวเองไปยัง varargs callee อีกตัวไม่ได้ วิธีแก้คือหยุดทำตัวเป็น wrapper: export naked routine ใต้ชื่อ C แล้ว tail-jump ไปยัง implementation จริง โดยใช้ argument register และ stack ตามที่ caller จัดไว้ทุกประการ ชั้น JPEG 2000 จัดการ snprintf และ vsnprintf ด้วยวิธีนี้อยู่แล้ว โดย jump ไปยังชื่อ msvcrt ที่มี underscore prefix เพราะชื่อปกติมีเฉพาะ UCRT ที่ export ข้อจำกัดที่เกี่ยวข้องอีกอย่างมาจาก Internal error เดียวกัน import cell ที่เปลี่ยนชื่อจะถูกเติมค่าจาก initialization section ผ่าน GetModuleHandleA และ GetProcAddress แทนที่จะใช้ static initializer เพราะการอ่าน address ของ imported routine ใน initializer ทำให้ compiler สร้าง fixup ที่จัดการไม่ได้ และกลับไปล้มที่ 200603061 อีกครั้ง
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Varargs forward จาก Pascal wrapper ไม่ได้ ดังนั้น exported
// symbol จึง tail-jump โดยคง stack frame ตามที่ caller ตั้งไว้
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
โปรเจกต์ Free Pascal ต้องทำอะไรต่างจากเดิม
ไม่มีอะไรนอกจากชื่อยูนิตใน uses clause และตอนนี้ก็ไม่ต้อง deploy ไฟล์ใดอีก backend register ตัวเองจาก initialization section ผ่าน RegisterJBIG2EncoderBackend และ caller ขอใช้งานเหมือนเดิมทุกอย่าง คือผ่าน option bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER ซึ่งมีค่า 4 หรือผ่าน argument UseExternalEncoder ของ extended entry point การขอใช้งานยังเป็นเพียง preference ไม่ใช่ guarantee เพราะ build ที่ลืมใส่ยูนิตจะ fallback ไปยัง native Pascal MMR encoder อย่างเงียบ ๆ และสร้างไฟล์ใหญ่ขึ้นแทนที่จะรายงาน error
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder และ Free Pascal ตั้งแต่ 3.538.0
var
Pdf: TPDFlib;
Scan: TStream;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
try
// Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
// BlackDotSize, LossyLevel
ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
finally
Scan.Free;
end;
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
มีข้อจำกัดสองอย่างที่ควรพูดให้ชัด ตอนนี้มี object set เฉพาะ Win64 ดังนั้นบน Free Pascal target อื่นทั้งหมด external encode entry point จะรายงาน failure และ native Pascal encoder จะทำงานแทน อีกข้อคือ regression ที่ใช้ตัดสินความถูกต้องของฟีเจอร์นี้เป็นการเปรียบเทียบภาพที่ render ไม่ใช่การตรวจขนาด เพราะ encoder ทั้งสองเป็น lossless เมื่อใช้ source เดียวกัน จึง render output แล้วเปรียบเทียบ byte ต่อ byte โดย Lazarus suite ผ่าน 26 จาก 26 test รวม test นี้ด้วย การเทียบขนาด compressed stream ไม่มีความหมาย เพราะหน้าที่กลับด้านจะบีบอัดได้ขนาดใกล้เคียงกับหน้าที่ถูกต้อง
บทเรียนนี้ใช้ได้ไกลกว่า JBIG2 DLL เป็นรูปแบบที่ถูกต้องเมื่อ boundary เป็น dynamic จริง ซึ่งเป็นกรณีที่ พื้นผิว integration ของ DLL, ActiveX และ dylib มีไว้รองรับ แต่เป็นรูปแบบที่ผิดเมื่อ DLL เป็นเพียง workaround ให้ COFF reader เพราะมันเพิ่มไฟล์ให้ทุก installer เพิ่ม search path ให้ทุก deployment และเพิ่ม failure mode จาก version skew ที่ static linking ไม่มี upstream ก็สำคัญเช่นกัน เพราะวิธีสร้างภาพสองระดับมีผลต่อขนาดสุดท้ายมากกว่าตัว encoder และ การ render ภาพขาวดำแบบแบ่ง region ใน Delphi ครอบคลุมอีกครึ่งของ pipeline ส่วน coverage ของ toolchain, object set แยกตาม compiler และ target ที่รองรับอยู่ใน หน้าผลิตภัณฑ์ losLab PDF Developer Library