Die PDFium-Komponente findet ihre Native-Bibliothek über eine feste, geordnete Suchkette, statt es dem Loader des Betriebssystems zu überlassen, denn ein expliziter Deployment-Baum ist ein Deployment-Baum, den man debuggen kann. Auf Windows sucht diese Kette ein Win32- oder Win64-Unterverzeichnis, das der Installer bereits mitliefert. Auf anderen Zielen baut sie den Unterverzeichnisnamen aus den Free-Pascal-Ziel-Macros auf, als <cpu>-<os>, sodass der Deployment-Baum exakt wie der kompilierte Unit-Baum liest. Diese letzte Entscheidung brachte einen Bug mit sich, der den ganzen Artikel wert ist, denn die Ursache war ein Großbuchstabe und das Symptom war Stille
Die Kette, der Reihe nach
Vier Orte, der Reihe nach versucht, dann der Plattform-Loader als letztes Mittel. Zuerst das bevorzugte Layout, ein DLLs-Verzeichnis neben der ausführbaren Datei mit einem Unterverzeichnis je Ziel. Zweitens ein alternatives Layout mit dem Ziel-Unterverzeichnis direkt neben der ausführbaren Datei. Drittens das flache Legacy-Layout, die Bibliothek neben der ausführbaren Datei ohne jegliches Unterverzeichnis. Viertens, nur auf Windows, das Systemverzeichnis, das Sorge braucht, denn ein 32-Bit-Prozess muss in SysWOW64 schauen und ein 64-Bit-Prozess in System32, und auf 32-Bit-Windows existiert Ersteres nicht, sodass die Suche zurückfallen muss. Erst nach all dem wird der Loader gebeten, selbst zu suchen
Außerhalb von Windows gibt es bewusst keinen Systemverzeichnis-Schritt. Der eigene Suchpfad des Plattform-Loaders, angetrieben von der Runtime-Linker-Konfiguration und der Bibliothekspfad-Umgebung, deckt dieses Gelände bereits ab, und es in Pascal zu duplizieren hieße, Regeln neu zu implementieren, die je Distribution variieren. Das Diagnostizieren von Fehlern in der Windows-Kette behandelt separat PDFium-DLL ausliefern und Ladefehler diagnostizieren
Woher der Unterverzeichnisname stammt
Auf Windows ist es Win32 oder Win64, entschieden durch die Bitbreite des laufenden Prozesses statt die des Betriebssystems, denn das bestimmt, welche Binärdatei geladen werden kann. Überall sonst wird der Name aus den Compiler-Ziel-Macros gebaut, sodass eine Maschine, die für zwei Architekturen baut, zwei klar getrennte Bäume produziert, und sodass der Ordner, der die Native-Bibliothek hält, neben dem Ordner liegt, der die kompilierten Units hält, mit demselben Namen
function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
if IsWin64 then
Result := 'Win64'
else
Result := 'Win32';
{$ELSE}
// Die Compiler-Macros schreiben das OS groß ("Linux", "Darwin"), das
// Unit-Ausgabeverzeichnis des Pakets nicht, also stimmen beide erst
// nach dem Folden überein. Auf einem case-sensitiven Dateisystem ist
// dieser Unterschied die ganze Suche
Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;
Warum ein Großbuchstabe die ganze Kette brach
Das Compiler-Macro schreibt das Zielbetriebssystem mit einem Anfangsgroßbuchstaben: Win64, Linux, Darwin. Das Lazarus-Paket schreibt seine Unit-Ausgabe in ein Verzeichnis, benannt nach seiner eigenen Zielvariablen, die kleingeschrieben ist: win64, linux, darwin. Zwei Schreibweisen desselben Dings, und keine Möglichkeit, es auf Windows zu bemerken, wo das Dateisystem sie nicht unterscheidet
Auf Linux sind es zwei verschiedene Verzeichnisse. Ein Deployment, das das Shared Object in DLLs/x86_64-linux ablegt, ist für einen Loader unsichtbar, der nach DLLs/x86_64-Linux sucht, also verfehlen alle vier expliziten Schritte der Kette, und der Code fällt durch zum Suchenlassen des Plattform-Loaders. Manchmal funktioniert das, wenn die Bibliothek zufällig systemweit installiert ist, und manchmal nicht, und in beiden Fällen trägt der sorgfältig angeordnete Deployment-Baum nichts bei. Der Fehler hat keine Fehlermeldung, weil nichts scheiterte: Jeder Schritt meldete korrekt, dass die Datei nicht dort war, wo er schaute
Das Sondenprogramm, kompiliert und ausgeführt
Diese Fehlerklasse findet man nicht durch Lesen, und durch Kompilieren auch nicht. Die übliche Technik, einen Plattformzweig zu verifizieren, der auf der Entwicklungsmaschine nie kompiliert, ist, die Unit in ein temporäres Verzeichnis zu kopieren, umzubenennen, das Plattform-Conditional durch ein nie definiertes Symbol zu ersetzen und die Kopie zu kompilieren; kompiliert sie, sind die Uses-Klausel und die Aufrufsignaturen auf diesem Pfad mindestens selbstkonsistent. Das funktioniert gut für eine in sich geschlossene Unit
Hier funktioniert das nicht. Die Haupt-Binding-Unit ist sehr groß und zieht die LCL herein, also kann sie nicht einfach kopiert und mit ausgeschaltetem Windows-Symbol kompiliert werden. Stattdessen wurden die Handvoll Funktionen, die die Änderung berührte, wortwörtlich in ein kleines in sich geschlossenes Programm transkribiert, und dieses Programm wurde ausgeführt. Es druckte x86_64-Win64, und die Diskrepanz war in einer Zeile Ausgabe sichtbar. Dasselbe Programm zu kompilieren hätte nichts gesagt, denn die Zeichenkette ist völlig gültig; nur ihr Wert ist falsch
program ProbeSubDir;
{$MODE DELPHI}
uses
SysUtils;
begin
// Drucken, nicht behaupten. Der Punkt ist, den Wert anzuschauen, zu
// dem ein Macro auf dieser Toolchain tatsächlich expandiert
Writeln('raw: ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
{$I %FPCTARGETOS%}));
end.
Die allgemeine Lektion: Wenn eine plattformübergreifende Änderung den Wert von etwas betrifft statt seinen Typ, ist Nur-Kompilieren-Verifikation keine Verifikation. Geben Sie es aus. Der weitere Satz an Cross-Compiler-Unterschieden zwischen Delphi und Free Pascal ist gesammelt in dem Artikel zu Delphi- und FPC-Cross-Compiler-Fallen
Die Plattform ihre eigenen Ladefehler erklären lassen
Der Windows-Zweig des Loaders zählt von Hand die Gründe auf, aus denen ein Laden scheitern kann, denn die nützlichen Unterscheidungen dort – eine Architekturdiskrepanz, eine fehlende transitive Abhängigkeit, ein Pfad, der sich nicht auflöst – mappen auf Fehlercodes, die es wert sind, einzeln benannt zu werden. Außerhalb von Windows gibt die portable Loader-Unit bereits eine beschreibende Zeichenkette zurück, die dasselbe Gelände abdeckt, also nutzt der Nicht-Windows-Zweig sie direkt, statt Kategorien aus einer Fehlernummer neu abzuleiten, die auf verschiedenen Systemen verschiedenes bedeutet
Dem Drang zu widerstehen, diese beiden in eine Meldung zu normalisieren, ist Absicht. Ein Ladefehler ist ein Deployment-Problem, und die Person, die die Meldung liest, braucht den eigenen Wortschatz der Plattform, um danach zu suchen
Eine Namenskollision, die rekursiert
Noch eine Falle, klein und scharf. Die portable Loader-Unit exportiert eine Prozedur namens UnloadLibrary, und die Binding-Unit hat eine Prozedur desselben Namens, die ihre eigene Buchhaltung macht, bevor sie das Handle freigibt. Innerhalb dieser Prozedur löst sich ein nicht qualifizierter Aufruf von UnloadLibrary zu der in der aktuellen Unit auf, die sich selbst aufruft. Der Fix ist, den Aufruf mit dem Unit-Namen zu qualifizieren
Das ist dieselbe Gestalt wie die Bezeichner-Verschattungsprobleme, die Free-Pascal-Ports allgemein dominieren: Die Windows-Unit exportiert Integer-typisierte Min- und Max-Funktionen, die die Gleitkomma-Varianten verschatten, und einen Synchronisationstyp, der die Klasse desselben Namens verschattet, und in jedem Fall hängt die Auflösung von der Reihenfolge der Uses-Klausel ab. Die Aufrufstelle zu qualifizieren ist der Fix, der nicht davon abhängt, dass jemand diese Reihenfolge später bewahrt
Deployment-Checkliste
Drei Dinge erklären die meisten Ladefehler, sobald die Pfadarithmetik stimmt. Die Architektur muss zum Prozess passen, nicht zur Maschine, also braucht eine 32-Bit-Anwendung auf 64-Bit-Windows die 32-Bit-Binärdatei. Der V8-aktivierte Build hat einen anderen Dateinamen, also sieht ein Deployment, das sie vermischt, korrekt aus und lädt nichts. Und nur eine Variante kann gleichzeitig in einem Systemverzeichnis leben, was ein guter Grund ist, das explizite Unterverzeichnis-Layout zu bevorzugen, statt irgendetwas systemweit zu installieren
Für Lazarus speziell: Legen Sie die Native-Bibliothek unter DLLs/<cpu>-<os> in Kleinschreibung ab, neben der ausführbaren Datei, und sie wird vom ersten Schritt der Kette auf jedem Ziel gefunden. Das Viewer-Beispiel, das das auf Lazarus ausübt, beschreibt der Artikel zum Lazarus- und FPC-Viewer, und die aktuelle Plattformunterstützung listet die Produktseite der PDFium Delphi-Komponente