Τεχνικό Άρθρο

Στατική σύνδεση του jbig2enc στο Free Pascal χωρίς DLL

Το PDFlibPas 3.538.0 συνδέει στατικά τον εξωτερικό encoder JBIG2 σε προγράμματα Free Pascal και Lazarus. Ένα project προσθέτει τη unit PDFlibJBIG2EncC, την ίδια unit που χρησιμοποιούν ήδη τα Delphi και C++Builder, και ο encoder καταλήγει μέσα στο εκτελέσιμο χωρίς να χρειάζεται να διανεμηθεί τίποτε επιπλέον δίπλα του. Αυτό ανατρέπει το προηγούμενο συμπέρασμα για τη δυνατότητα, σύμφωνα με το οποίο το Free Pascal μπορούσε να φτάσει τον εξωτερικό encoder μόνο μέσω DLL

Γιατί η DLL έμοιαζε με τη μοναδική επιλογή

Η DLL έμοιαζε με τη μοναδική επιλογή επειδή τρεις διαδρομές σύνδεσης αποτύγχαναν με τρεις άσχετους μεταξύ τους τρόπους και κανένας διακόπτης του compiler δεν έφτανε σε καμία από αυτές. Ο εσωτερικός linker απορρίπτει αμέσως τα associative COMDAT sections. Μια εξωτερική σύνδεση με τα binutils που περιλαμβάνονται καταρρέει μέσα στη συλλογή sections, την οποία το Free Pascal περνά χωρίς όρους στον στόχο 64-bit Windows. Νεότερα binutils δεν μπορούν να επεξεργαστούν καθόλου το script σύνδεσης του Free Pascal. Η επαναδημιουργία της πλευράς C++ με την άλλη toolchain ανταλλάσσει μία άρνηση με μια άλλη, επειδή οι instantiations templates και inline εκδίδουν εκ φύσεως weak external symbols και το Free Pascal τα αναφέρει ως Unsupported COFF symbol type 105. Καμία από αυτές τις ενδείξεις δεν ήταν λανθασμένη, και η προηγούμενη περιγραφή των backends του encoder JBIG2 και του linker του Free Pascal διατρέχει κάθε αδιέξοδο με τρόπο που εξακολουθεί να αναπαράγεται σήμερα. Λανθασμένη ήταν η υπόθεση για το πού μπορούσε να βρίσκεται η διόρθωση. Κάθε προσπάθεια περνούσε από compiler ή linker, και κανένας από τους δύο δεν μπορεί να αλλάξει ό,τι περιέχει ήδη ένα object file. Το object file ήταν εξαρχής το πρόβλημα. Το ObjConv διαβάζει COFF και γράφει COFF, και κάθε κατασκευή στην οποία σκοντάφτει το Free Pascal έχει ένα μηχανικό ισοδύναμο που αποδέχεται

Το σφάλμα που δεν ονομάζει ποτέ την αιτία του

Ο εσωτερικός linker του Free Pascal υλοποιεί το pick-any COMDAT μόνο κατά το ήμισυ, και αυτή η μισή υλοποίηση είναι το δυσκολότερο σημείο στη διάγνωση. Συγχωνεύει πράγματι τους διπλούς ορισμούς, όπως προβλέπει το format. Όμως το TExeOutput.RemoveUnreferencedSections ανακατευθύνει μέσω του exesymbol στον ορισμό που κέρδισε όταν σημειώνει τα sections ως χρησιμοποιούμενα, ενώ το TCoffexeoutput.DoRelocationFixup διαβάζει απευθείας το objreloc.symbol.objsection. Όταν ένα χρησιμοποιούμενο section αναφέρεται σε symbol που το δικό του object ορίζει σε αντίγραφο το οποίο έχασε τη συγχώνευση, τα δύο περάσματα κοιτούν διαφορετικά sections και η σύνδεση σταματά με 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, ούτε όνομα αρχείου, ούτε φάση. Είναι επίσης η κανονική περίπτωση και όχι μια σπάνια γωνιακή περίπτωση, επειδή το MSVC τοποθετεί κάθε string literal και κάθε instantiation inline ή template σε pick-any COMDAT, ενώ στα 186 objects αυτού του encoder ο linker έκανε 2656 συγχωνεύσεις. Το build με /Gy- κρατά τις συνηθισμένες functions έξω από τα per-function COMDAT sections, αλλά αφήνει τα string literals και τα template instantiations ακριβώς εκεί που ήταν

Γιατί το stubbing των CRT symbols μοιάζει πάντα να χαλάει το τελευταίο

Επειδή ο linker φτάνει στο pass των fixups μόνο αφού επιλυθεί κάθε symbol. Όσο λείπει κάτι, η εκτέλεση τελειώνει νωρίς με Undefined symbol και το πρόβλημα COMDAT δεν προλαβαίνει να εμφανιστεί. Συμπλήρωσε το τελευταίο stub του C runtime και ο linker προχωρά μία φάση, κατευθείαν στο internal error 200603061. Το σύμπτωμα στο πεδίο είναι επομένως συστηματικά παραπλανητικό: όταν προσθέτεις ένα-ένα τα Pascal bodies για τα C symbols που αναφέρονται, μοιάζει πάντα σαν να χάλασε το build η πιο πρόσφατη προσθήκη ή σαν να ξεπεράστηκε κάποιο όριο γύρω στα εκατό stubs. Τίποτε από τα δύο δεν ισχύει. Ποιο symbol μπήκε τελευταίο και πόσα μπήκαν συνολικά είναι αδιάφορο, επειδή η αποτυχία ήταν κρυμμένη από το πρώτο object και έγινε προσβάσιμη μόνο όταν ολοκληρώθηκε επιτυχώς η επίλυση. Όταν ένας linker αλλάζει μήνυμα αφού διορθώσεις κάτι άσχετο, ρώτησε αν απλώς προχώρησες μια φάση αντί να προκαλέσεις regression

Η διόρθωση είναι ένα pass του ObjConv, όχι flag του compiler

Ολόκληρη η διόρθωση είναι μία εντολή post-processing πάνω σε κάθε compiled object: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Τρεις από αυτές τις επιλογές προστέθηκαν για αυτή τη δουλειά. Το -xw μετατρέπει τα symbols IMAGE_SYM_CLASS_WEAK_EXTERNAL σε συνηθισμένα externals. Το -xn κανονικοποιεί τα symbols IMAGE_SYM_CLASS_NULL, όπως το _fltused, τα οποία το Free Pascal αναφέρει ως Unsupported COFF symbol type 0. Το -xc κάνει τη βαριά δουλειά: υποβιβάζει κάθε COMDAT section σε απλό section και κάνει static τα symbols που ορίζει. Έτσι η αποτυχία εξαφανίζεται επειδή εξαφανίζεται η απόφαση: χωρίς COMDAT sections δεν υπάρχει συγχώνευση, ούτε αντίγραφο που κερδίζει ώστε το ένα pass να ανακατευθύνει και το άλλο να μην το βρει, ενώ μαζί του εξαφανίζονται και τα associative .pdata και .xdata unwind sections. Το κόστος υπάρχει αλλά είναι μικρό, αφού αντίγραφα που θα μπορούσαν νόμιμα να συγχωνευτούν παραμένουν πλέον ξεχωριστά

Η μετονομασία του prefix -np:__imp_:pdflibimp_ λύνει μια διαφορετική σύγκρουση. Το MSVC καλεί τα imported Win32 APIs μέσω cells έμμεσης σύνδεσης με ονόματα __imp_*, το Free Pascal κρατά αυτό το prefix για τον δικό του μηχανισμό imports, και ο άμεσος ορισμός ενός τέτοιου ονόματος προκαλεί το ίδιο internal error 200603061. Με τη μετονομασία, η Pascal πλευρά μπορεί να εκθέσει τα cells ως απλές variables και να τα συμπληρώσει κατά το runtime. Τα objects μεταγλωττίζονται με ενεργό το static-link flag, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, και με τους image codecs απενεργοποιημένους, οπότε οι νεκρές διαδρομές file-I/O και codec χρειάζονται πολύ λιγότερα stubs μόνο για τη σύνδεση. Καταλήγουν στο Lib\thirdparty\Win64f, ενώ η διαδρομή Delphi και C++Builder συνεχίζει να συνδέει ανέγγιχτο το δικό της σύνολο Win64x, που είναι το σωστό αποτέλεσμα για μια διόρθωση φορητότητας περιορισμένη σε μία toolchain

Τι πρέπει ακόμη να εξάγει η Pascal πλευρά

Το Free Pascal επιλύει το import ενός C object με βάση το όνομα του symbol, οπότε κάθε Pascal routine που αντικαθιστά ένα C entry point χρειάζεται ρητή ρήτρα public name. Το Delphi χρησιμοποιεί το όνομα της routine ως όνομα symbol και δεν χρειάζεται καμία ρήτρα, γι’ αυτό μία unit εξυπηρετεί και τους δύο compilers με τις ρήτρες κάτω από {$IFDEF FPC}. Η παγίδα είναι ότι μια δήλωση external 'msvcrt.dll' δεν ικανοποιεί τίποτε: δημιουργεί import, ποτέ definition με την οποία μπορεί να δεθεί ένα linked object. Πρέπει να υπάρχει το forwarding body

// Μια external declaration δημιουργεί μόνο import. Κανένα linked object δεν μπορεί
// να δεθεί σε αυτή
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Ένα Pascal body που δημοσιεύεται κάτω από το ακριβές C symbol name είναι αυτό
// στο οποίο δένεται πραγματικά το σύνολο των objects
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Τα variadic entry points σπάνε αυτό το μοτίβο, επειδή ένα Pascal wrapper δεν μπορεί να προωθήσει τα δικά του varargs σε άλλον variadic callee. Η λύση είναι να πάψεις να είσαι wrapper: εξάγαγε μια naked routine κάτω από το C name και κάνε tail-jump στην πραγματική υλοποίηση με τα argument registers και το stack ακριβώς όπως τα έστησε ο caller. Το layer JPEG 2000 χειρίζεται ήδη έτσι τα snprintf και vsnprintf, κάνοντας jump στις spellings του msvcrt με underscore prefix επειδή τα απλά ονόματα εξάγονται μόνο από το UCRT. Ένας σχετικός περιορισμός προέρχεται από το ίδιο internal error: τα renamed import cells συμπληρώνονται από section initialization μέσω GetModuleHandleA και GetProcAddress και όχι από static initializers, επειδή η λήψη της διεύθυνσης μιας 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 δεν μπορούν να προωθηθούν από Pascal wrapper, οπότε το exported
// symbol κάνει tail-jump με το frame του caller ακριβώς όπως έχει στηθεί
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Τι κάνει διαφορετικά πλέον ένα project Free Pascal

Τίποτε πέρα από το όνομα της unit στη uses clause, και δεν υπάρχει πλέον αρχείο για διανομή. Το backend εγγράφεται από το δικό του initialization section μέσω του RegisterJBIG2EncoderBackend, και οι callers το ζητούν όπως πριν: μέσω του option bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER, που έχει τιμή 4, ή μέσω του ορίσματος UseExternalEncoder των extended entry points. Το αίτημα παραμένει προτίμηση και όχι εγγύηση, επειδή ένα build που παραλείπει τη unit επιστρέφει αθόρυβα στον native Pascal MMR encoder και παράγει μεγαλύτερα αρχεία αντί για error

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder και, από 3.538.0, Free Pascal

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, άρα σε κάθε άλλο target του Free Pascal το external encode entry point αναφέρει αποτυχία και αναλαμβάνει ο native Pascal encoder. Και το regression που ελέγχει όλο αυτό είναι σύγκριση render και όχι μεγέθους: και οι δύο encoders είναι lossless στην ίδια πηγή, οπότε η έξοδός τους γίνεται render και συγκρίνεται byte προς byte, με τη σουίτα του Lazarus να περνά και τα 26 από τα 26 tests μαζί με αυτό. Η σύγκριση μεγεθών των compressed streams δεν θα αποδείκνυε τίποτε, επειδή μια ανεστραμμένη σελίδα συμπιέζεται περίπου στο ίδιο μέγεθος με μια σωστή

Το ευρύτερο μάθημα ισχύει πέρα από το JBIG2. Μια DLL είναι το σωστό σχήμα όταν το boundary είναι πραγματικά δυναμικό, όπως στην περίπτωση που εξυπηρετούν οι επιφάνειες integration για DLL, ActiveX και dylib· είναι λάθος σχήμα όταν αποτελεί απλώς workaround για έναν COFF reader, επειδή προσθέτει ένα αρχείο σε κάθε installer, ένα search path σε κάθε deployment και έναν τρόπο αποτυχίας λόγω version skew που δεν μπορεί να έχει το static linking. Μετρά και το προηγούμενο στάδιο, επειδή ο τρόπος παραγωγής της bilevel εικόνας επηρεάζει το τελικό μέγεθος περισσότερο από τον encoder, και το region-based monochrome rendering στο Delphi καλύπτει αυτό το μισό του pipeline. Η κάλυψη των toolchains, τα object sets ανά compiler και τα υποστηριζόμενα targets παρατίθενται στη σελίδα προϊόντος losLab PDF Developer Library