הנה בעיה שמופיעה ברגע שספריית PDF יוצאת משפת הבית שלה. יש לכם קישור שעובד מצוין מ-C# על Windows. אתם צריכים את אותן קריאות מ-Python על macOS, אז אתם מעתיקים את קובץ הצהרת Windows, מחליפים את שם הבינארי, ומריצים. כל סמל נפתר. הקריאה הראשונה מחזירה זבל, השנייה קורסת עם הפרת גישה, ואף שורת PDF code שלכם לא השתנתה. הבעיה נמצאת שכבה מתחת ל-PDF: ייצוא Windows משתמש בקונבנציית Stdcall, ואילו ה-dylib של macOS מייצא את אותן פונקציות כ-Cdecl עם קו תחתון מוביל, וצהרת FFI שמפספסת אחד מהפרטים האלה מקלקלת את ה-stack לפני שמסמך אחד נפתח
כל מחלקת כשל זו נובעת מהחלטת עיצוב אחת שכדאי להבין מראש. PDFlibPas, מנוע ה-PDF עם קוד מקור זמין של losLab עבור Delphi ו-C++Builder, עוטף את כל מודל האובייקטים שלו במחלקת חזית שטוחה אחת, TPDFlib, ואז שולח חזית זו בשלוש צורות בינאריות: DLL ל-Windows עם כ-1,250 פונקציות מיוצאות, אובייקט אוטומציה COM/ActiveX, ו-dylib ל-macOS. הסמנטיקה של PDF זהה בכל השלושה. הדבר שנושך אתכם נמצא ב-ABI שמתחת: קונבנציות קריאה, קידוד מחרוזות, בעלות על מזהים, ואיזה צד רשאי לשחרר איזה buffer
חזית אחת, שלוש צורות בינאריות
לכל פונקציה ציבורית של TPDFlib יש מקבילה שטוחה בשם DL ואחריו שם המתודה. LoadFromFile הופכת ל-DLLoadFromFile, Encrypt הופכת ל-DLEncrypt, NewSignProcessFromFile הופכת ל-DLNewSignProcessFromFile. הפרמטר הראשון של כמעט כל ייצוא הוא InstanceID שמוחזר על ידי DLCreateLibrary, שמחליף את הפניה לאובייקט שקורא Delphi היה מחזיק אחרת. פנימו את המיפוי הזה מוקדם. זה אומר שהפניה ל-API של Delphi משמשת גם כתיעוד לכל שפה אחרת: כל מה שהמחלקה יכולה לעשות, ה-DLL יכול לעשות תחת שם צפוי, ואתם יכולים לקרוא חתימת מתודה של Pascal ללמוד את הקריאה שאתם צריכים מ-Python או C#
הבנייה ל-Windows מייצרת PDFlibDLL32.dll ו-PDFlibDLL64.dll; בחרו את זה שמתאים לקצב ה-bit של תהליך המארח שלכם, שכן תהליך Java או .NET של 64 סיביות לא יכול לטעון את הספרייה של 32 סיביות לא משנה איך הצהרה נראית
Windows: מופעי Stdcall וזוגות פונקציות W/A
כל ייצוא שמקבל מחרוזת קיים פעמיים. גרסה רחבה מקבלת PWideChar (UTF-16, ההתאמה הטבעית ל-.NET, Java ו-c_wchar_p של Python), וגרסה עם סיומת A מקבלת PAnsiChar. שתיהן נושאות סמנטיקה זהה ושונות רק בקידוד, וזה בדיוק מה שהופך את ערבובן לכל כך כואב לעקוב: שום דבר לא זורק שגיאה, שום דבר לא מחזיר קוד שגיאה, אתם פשוט מקבלים mojibake במטא-דאטה או "file not found" מזויפת לכל נתיב עם תו מעבר ל-ASCII רגיל. הבאג הראשון של קידוד שצוות פוגש בדרך זו בדרך כלל עולה אחרי הצהריים, כיוון שהסימפטום מצביע על הנתונים והגורם נמצא בהצהרה
// Windows binding (PDFlibDLL64.dll): Stdcall, plain export names
function DLCreateLibrary: Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
FileName, Password: PWideChar): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLLoadFromFile';
// macOS binding: same function, Cdecl, and an underscore prefix on the export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
בחרו רוחב תו אחד לכל מארח וקדדו אותו בגנרטור הקישורים. כלל מעשי: אם לשפת המארח יש מחרוזות UTF-16 מקוריות, קשרו את גרסאות W בכל מקום ואל תגעו יותר במשפחת A
macOS: אותם שמות, ABI שונה
ה-dylib מייצא את אותה קבוצת פונקציות DL עם שני שינויים שיטתיים. קונבנציית הקריאה היא Cdecl ולא Stdcall, וכל שם ייצוא נושא קו תחתון מוביל (_DLCreateLibrary, _DLLoadFromFile, וכן הלאה). שני השינויים הם מכניים לחלוטין, מה שהופך אותם לאידיאליים לקישור שנוצר ומסוכנים לעותק שנערך ביד של קובץ ה-Windows. שמרו רשימת פונקציות קנונית אחת ופלטו הצהרות לכל פלטפורמה ממנה אם הכלים שלכם מאפשרים. דלגו על זה ותקבלו בדיוק את השחתת ה-stack שתוארה בתחילת הדף, שמשחזרת רק על הפלטפורמה שה-CI שלכם פחות מבצע עליה
מארחי COM ו-ActiveX: Safecall ומטענות Olevariant
עבור VB.NET, C#, VBScript ומארחי אוטומציה מדור קודם, בנייה OCX עוטפת את אותה חזית באובייקט אוטומציה IDispatch, IPDFlibrary, עם כל מתודה שמוצהרת Safecall. קונבנציה זו משנה איך שגיאות מגיעות אליכם. Safecall מתרגמת כשל פנימי ל-COM HRESULT, כך שקורא C# תופס חריגה היכן שה-DLL השטוח היה מחזיר integer שקט שהקורא היה צריך לזכור לבדוק. אותה פעולה, שתי ניחוחות כשל, תלוי איזה בינארי טענתם
נתונים בינאריים עוקבים אחרי כלל ספציפי שני של COM. לממשק האוטומציה אין פרמטרים מצביעים בכלל. כל דבר בינארי, בתי תמונה שנכנסים או בתי PDF שיוצאים, חוצה את הגבול כ-Olevariant דרך מתודות כמו AddImageFromVariant ו-AppendToVariant. Marshaling של מערך bytes ל-variant הוא שורה אחת ב-.NET. נסו להעביר מצביע גולמי במקום, בנימוק שזה אותו תהליך בכל מקרה, ושכבת ה-dispatch דוחה או מפגרת את הקריאה. עוד פרט רישום אחד מכשיל פריסות: רישום COM הוא לכל-קצב-bit, אז OCX רשום עם regsvr32 של 32 סיביות אינו נראה למארח של 64 סיביות. אי-התאמה זו מתגלה כ-"class not registered" המסייעת בדרך כלל ביותר במחשב הלקוח, הרבה אחרי שעזב את שלכם
משמעת מזהים: מופעים מחזיקים מסמכים
ה-API השטוח פועל על מזהים שלמים. DLCreateLibrary מחזיר מופע. טעינת קובץ מחזירה מזהה מסמך בתוך אותו מופע. תהליכי חתימה, רשימות מחרוזות וקבצי גישה ישירה מחזירים מזהים שלמים משלהם, כולם בתחום אותו מופע. מחזור החיים נראה אותו הדבר מכל מארח FFI, מוצג כאן ב-Pascal כיוון שהוא נקרא בצורה נקייה:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // one instance per worker thread
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returns a DocumentID, 0 on failure
if Doc <> 0 then
begin
DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
DLSaveToFile(Inst, 'out.pdf');
end;
finally
DLReleaseLibrary(Inst); // frees every document the instance owns
end;
end;
שני דברים נובעים מעץ הבעלות הזה. DLReleaseLibrary היא קריאת הניקוי היחידה שאתם צריכים בקפדנות, שכן היא מפרקת כל מסמך ומזהה תהליך תחת המופע בפעולה אחת. בסקריפט קצר זה מספיק. בשירות שרץ לאורך זמן זה הופך לדליפה איטית עם טקס מיותר, אז שחררו מסמכים כשסיימתם איתם במקום לתת להם להצטבר עד שהמופע מת. המופע הוא גם יחידת הבידוד הטבעית של thread. תנו לכל thread עובד InstanceID משלו, ואל תשתפו אחד בין threads ללא נעילה חיצונית, מאותה סיבה שלעולם לא תשתפו אובייקט TPDFlib יחיד בין threads
מחרוזות שמוחזרות הן שאולות, לא בבעלותכם
פונקציות שמחזירות טקסט, כמו DLGetPageText, מחזירות PWideChar או PAnsiChar שמצביע לתוך buffer שבבעלות המופע של הספרייה ומוחזר על ידה. החוזה הוא: העתיקו מיד, לעולם אל תשחררו
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copy now; a later call may reuse the buffer
end;
ב-C# זה אומר marshal של IntPtr למחרוזת מנוהלת לפני קריאת הספרייה הבאה. ב-Python ctypes, זה אומר חיתוך המחרוזת הרחבה מהמצביע מיד. החזיקו את המצביע הגולמי בין קריאות וכתבתם באג שעובר כל בדיקת unit ואז נכשל בפעם הראשונה ששתי בקשות חופפות בייצור, כיוון שהקריאה השנייה מחזרה את ה-buffer שהראשונה עדיין קוראת. אותה כלל בעלות פועל בכיוון ההפוך עבור callbacks שנרשמו דרך DLSetProgressCallback. כל מצביע שהספרייה מעבירה ל-callback שלכם תקף רק בתוך גוף ה-callback הזה, ואובייקט ה-callback עצמו חייב להישאר חי (מוצמד, במארח עם garbage collection) כל עוד המופע עשוי עדיין לקרוא לו. delegate שנאסף באמצע עבודה הוא המקור הלימודי של הפרת הגישה "האקראית" שמופיעה בקישור .NET שרץ בניקיון במשך חודשים
בנו בדיקת smoke לתוך הקישור עצמו, והריצו אותה לפני שסט הצהרות שנוצר נשלח. תרגלו קריאה אחת מכל קטגוריה שנוטה לחשוף טעויות ABI: פונקציה ללא פרמטרים כמו DLCreateLibrary להוכחת נכונות הקונבנציה, פונקציה עם מחרוזת-פנים שניזונה בנתיב עם תווים שאינם ASCII להוכחת נכונות הקידוד, פונקציה עם מחרוזת-חוץ להוכחת נכונות טיפול ה-buffer השאול, ופעולה אחת שנכשלת בכוונה כדי שתוכלו לצפות כיצד שגיאה מגיעה למארח שלכם. זה חמש עשרה דקות עבודה, והיא תופסת את פגמי קונבנציית הקריאה והקידוד שאחרת יגיעו חודשים מאוחר יותר כ-dump קריסה של לקוח
מקרה ה-Python ctypes, בצורה קונקרטית
Python ctypes הוא הקישור שאני רואה מגולגל ביד לעתים קרובות ביותר, והוא מקל להדגים את הפיצול חוצה-פלטפורמה. על Windows, טענו את הספרייה עם ctypes.WinDLL כדי ש-ctypes יישם Stdcall, קשרו את פונקציות W ללא סיומת, והצהירו כל פרמטר מחרוזת כ-c_wchar_p. על macOS, טענו אותה עם ctypes.CDLL עבור Cdecl, השאירו את רשימת הפונקציות הזהה, ופתרו שמות ללא קו תחתון מוביל. רוב שכבות FFI, ctypes כלולה, מקפלות בחזרה את קונבנציית קו-התחתון עבורכם על macOS, אך זה ההנחה האחת לאשר עם קריאה בודדת שנפתרה לפני שאתם מייצרים מאות הצהרות על גבי זה
שתי שאלות פריסה עוקבות את עבודת הקישור ויש להן תשובות ברורות. ה-DLL הפשוט אינו צריך רישום: regsvr32 חל רק על בנייה ActiveX, וה-DLL נשלח על ידי העתקת קובץ, וזו הסיבה העיקרית להעדיפו לשירותי Windows וקונטיינרים שבהם תעדיפו לא לגעת ב-registry כלל. בטיחות Thread מצטמצמת לכלל שכבר פועל לעיל, מופע אחד לכל thread. מזהה המופע מחזיק כל פיסת מצב גמיש שהמנוע עוקב אחריו, המסמך שנבחר, אפשרויות הרנדור, הגדרות החילוץ, כך שני threads ששותפים מופע מסיטים את מצב אחד של השני גם כאשר כל קריאה בודדת מחזירה הצלחה
ברגע שהקישור מוצק, הפעולות בצד השני שלו הן בדיוק אלה שמאמרי Delphi מכסים לעומק, כולל יישום וביקורת הצפנת PDF וחילוץ טקסט ותמונות ממסמכים קיימים
הורדות בינאריות לכל שלוש שכבות האינטגרציה נשלחות עם הספרייה; ראו את דף המוצר PDFlibPas עבור מהדורות ורישוי