הבלוג של ארתיום
בלוג על לינוקס, תוכנה חופשית, מוזיקה, סלסה, ומה לא!
אני ו-dll או defected by design
בשבוע האחרון עבדתי על כתיבת טלאי לפרויקט soci שיאפשר טעינה דינמית של מודולים. הפרויקט הוא בלתי תלוי בפלטפורמה ויודע לעבוד הן על פלטפורמת POSIX והן על Win32. אז הייתי צריך לתמוך ביצירה וטעינה של dllים ושל soים לפי הצורך.
למרות ש-Win32API ו-POSIX API הם שונים לא הייתה שום בעיה לספק את התמיכה בטעינה דינמית של ספריות... בסה"כ להגדיר כמה defineים שכך שבפלטפורמה תואמת POSIX אני קורא ל-dlopen וב-Win32 אני קורא ל-LoadLibrary, באחת אשתמש ב-CriticalSection ובשני ב-PThread Mutex ועוד.
הכל היה טוב יפה עד שהגעתי ליצירה נכונה של הספריה עבור Win32. אז נחשפתי למספר דברים ששפכו הרבה אור על נושא של dllים ב-Windows וכמה "השלכות מוזרות" שבתור מי שמפתח ב-Linux לא נתקלתי בהם.
ובכן נתחיל.
כידוע אם אתה עובד עם Autotools ומייצר ספריה כברירת מחדל אתה מקבל שני עותקים שלה: אחד מודול שמאפשר טעינה דינמית -- so. והשני ספריה סטטית או a.. בתור מי שרגיל לזה שכמעט כל ספריה מסופקת כמודול דינמי התרגלתי לחיים טובים. בזמנו, כשבחנתי אפשרות לעבוד עם cmake נתקלתי במשהו מוזר שדיבר על כך ש"בכל מקרה כשבונים ספריה דינמית וסטטית צריך לקמפל פעמיים"... מוזר, לא?
כל מי שמפתח ל-Windows יודע היטב את ההבדל בין lib לבין dll ומכיוון ש-cmake תומך גם ביצירת פרויקטים ל-Visual Studio אז זה הגיע גם אליו.
ב-Windows אתה צריך להבחין בין שלושה מקרים שונים:
- קבצי header/source שאמורים לשמש ביצירה של dll -- הם חייבים להכיל dllexport לכל מחלקה או פונקציה.
- קבצי header שאמורים לשמש את הפרויקט שמתקמפל עם הספריה דינמית ואמורים להכין הגדרות dllimport לכל מחלקה ופונקציה.
- קבצי header/source שאמורים לשמש ליצירת ספריה סטטית או lib וגם לצורך להתקמפל איתה סטטית -- ללא שום dllexport/import.
כל הסיפור הזה מנוהל ברמת defineים. כשאני מקמפל dll אני חייב להגדיר define שיגיד ליש להכניס dllexport לקבצים. כשאני מקמפל ספריה סטטית אני צריך define שיגיד להכניס הגדרה ריקה (ללא export/import) - שזה גם אומר שיש לקמפל פעמיים כדי לקמפל lib ו-dll.
כשעובדים עם dll יש להגדיר define ברמת הפרויקט כדי לקבל הגדרות dllimport -- שימו לב, זה define שונה מזה ששימש ליצירה של אותו dll וכשרוצים לעבוד עם ספריה סטטית צריך גם define אחר. לא פלא, שבדר"כ ב-Windows לא כל כך אוהבים לעבוד עם dllים. ובד"כ לא מייצרים שני גרסאות שונות של אותו קוד לספריה סטטית וגם ל-dll.
אתם חושבים שזה מה שמתסכל אותי? אז לא סיימתי...
בתוך השיחה עם חבר שלי התלוננתי על כל הסיוט הזה ואז קיבלתי תשובה חותכת שבהתחלה לא הבנתי במה מדובר:
פ': אבל אתה בד"כ בוחר רק סוג אחד של ספריה או dll או lib. כך שזה לא ממש לא נורא כשמתרגלים אני: למה? מה מפריע לי לבחור לעבוד עם אחד או שני הרי זה אותו דבר פ': מה פתאום! זה לא אותו דבר יש דברים שמתנהגים שונה ב-dll וב-lib? למשל משתנים סטטיים? אני: הייתכן? אבל אני מעולם לא הבחנתי בשום הבדל בין so לבין ספריה סטטית? מדוע אמור להיות הבדל?
אז כתבתי תוכנה קצרה וקימפלתי אותה במכונת לינוקס שהדגימה אין שום הבדל! מה שהביא את פ' להתפלאות לא מעטה... לאחר מכן, לקחתי את אותו הקוד ויצרתי dll ואכן גיליתי שזה מתנהג שונה.
הנה קטע קוד שיראה את ההבדל?
mylib.h: class X{ public: int a() { static int n; return n++; }; int b(); };
mylib.cpp: int X::b() { return a(); };
test.cpp int main() { X x; cout<<x.a()<<'n'; cout<<x.b()<<'n'; cout<<x.a()<<'n'; cout<<x.b()<<'n'; return 0; }
מה אתם מצפים שיהיה הפלט של הקוד? 0, 1, 2, 3? אז בעבודה עם dll הפלט יהיה 0, 0, 1, 1!!! כמובן שזה יהיה הפלט הקודם 1--3 כשאני אקמפל את הקוד בתור lib. למה? כי ב-dll יש עותק של int n והן בקוד הראשי יש עותק של אותו int n והם שונים...
אז מה אפשר יכול להגיד אדם שפוי שרגיל לעבוד עם דברים הגיוניים על dll? כנראה Defected By Design!
הערה: כל אלה הם השרידים של Win16api שבו באמת היו לא מעט בעיות לא טריוויאליות שדרשו פתרונות כלה... אבל התכנון הזה נשאר אפילו בעידן של 64 ביט!
לא! אני לא רוצה לתמוך בפלטפרמת Win32 אם אני רוצה להשאר שפוי בדעתי...
הערה 2: כמובן שבסוף הכנתי את התמיכה ב-dll גם ל-soci.
הערה 3: הסיבה להבדל בין התנהגות של so לבין התנהגות של dll הוא בדרך בה פותרים בעית הטעינה של ספריה דינמית ובהבדלים בין DLL PE לבין ELF. הם פשוט עובדים שונה...
הערה 4: לא, אני לא שונא Windows... פשוט כל הכרות יותר ויותר מעמיקה שלי איתו גורמת לי לחבב את לינוקס יותר ויותר :)
תגובות
שמע שכחת לדעתי עוד כל מיני דברים בנושא של DLL, אבל מצד שני, יש בתכנות ב windows הרבה דברים מעצבנים בלי קשר... למשל בעולם ה"רגיל" אתה תשתמש ב main' אבל בWindows אתה תשתמש ב WinMain. ה Calling Convention של דברים בWindows השתנה יותר מפעם אחת בין גרסאות שלה. התאימות לאחור מחד גיסא והרצון לבטל טכנולוגית בשביל לקדם טכנולוגיות אחרות גורמות לכך שלפעמים השקעה מאוד גדולה שעשית עבור טכנולוגיה אחת, לפעמים במשך 3-4 שנים, פתאום תהיה מיותרת לגמרי בגרסה חדשה, או אפילו בגלל עדכון זה או אחר.
אני יכול להמשיך כמובן.... הרבה אנשים הולכים לפתרון של שפות כדוגמת Java, פיתון פרל וכו'... למזלי FPC השכילו לפחות במקרה למעלה לפשט את זה לרמה שהיציה בעיני של דברים תהיה זהה, אלא אם אני ארצה לשלוט בדברים בעצמי (דבר שאפשרי). בתור אחד שתכנת כמה שנים מעל Windows, אני חייב להגיד שאני לא מתגעגע ולו יום אחד לעולם הזה, למרות שההכרות המעמיקה שלי בAPI (שהיה).
בלי קשר לנפלאות ה-Windows :-) גם במערכות Linux/Unix מקמפלים (בדרך כלל) בנפרד קוד עבור so ועבור ספריות סטאטיות, אבל מסיבות שונות לגמרי: - קוד עבור ספריות דינמיות צריך להיות בלתי תלוי בכתובת הטעינה -- PIC. - קוד עבור ספריות סטאטיות לא מקובל לקמפל עם PIC כדי לא להפסיד ביצועים (בפלטפורמות מסויימות, כגון PPC, זה לא נכון).
אם כבר הזכרת את autotools שמקל קצת על קשיי התאימות, כדאי להציץ ב-libltdl (חלק מ-libtool) שמספק עטיפה פורטבילית ל-dlopen, LoadLibrary וחבריהם.
oron -- אתה צודק, רק שבפועל, ההבדל בין PIC לבין רגיל לא עד כדי כך משמעותי (אם כי יש לזה מחיר). בכל אופן, בגלל שלרוב משתמשים ב-so אז כך גם ברירת המחדל.
לגבי libltdl, זה נכון, רק שלא רציתי להכניס עוד תלות לפרויקט soci בנוסף לזה ש-libltdl מסופק תחת LGPL כש-soci תחת boost וזה יכול להוות בעיה מסויימת כך שרציתי למנוע בעיות כאלה.
חוץ מזה, libltdl נועד לא רק ל-Win32 אלא גם ל-UNIXים שלא תומכים ב-dlopen. אבל היום המצב יותר טוב. למשל ב-HP-UX יש כבר dlopen (לפחות לפי man שלהם) כך שזה פחות ופחות בעיה.
נכון... יש הרבה דברים שמשגעים אותך ב-Win32API. מה שעוד יותר מדהים אותי, שיש אנשים שחושבים ש"זה המצב הטבעי והנכון"...
לפחות מתוך השיחה שלי עם פ' הבנתי שכמות ההבדלים בין העולם האמיתי לבין העולם לפי MS מאוד מרתיע אותו כי זה יהיה קשה מאוד ללמוד מה קורה בחוץ.
הוסף תגובה:
חובה לאפשר JavaScript כדי להגיב.