בהמשך לפוסט הקודם – אני מוצא את עצמי מפתח מערכות גם בWindows חזרה.
יצרתי באמצעות שפת C++ (וקצת C) איזשהו DLL המבצע קיצורי קוד עבורי שבכל דרך אחרת הייתי צריך לממש הרבה COM בשפת Go – כאב ראש עם הרבה boilerplate code של Vtbl (Virtual Function Table) ומימוש Interfaceים שלמים, כולל ירושות שלהם מinterface אחרים.
אני מאוד אוהב את שפת Go אבל יש לי גם ביקורת קשה כלפיה – הביקורת העיקרית שלי (לצורך הפוסט כי יש לי יותר מביקורת מאחת) – הוא שמצד אחד ניסו לספק לי קוד "גבוה" שלא מעניין אותו מערכת ההפעלה, כאשר מהצד השני, רואים שהוא נבנה עבור Unix/Linux וכאשר אנו נמצאים במערכת הפעלה שונה כדוגמת Windows מערכת ההפעלה מרגישה שם כ second class citizen.
בעיה ראשונה – יש לי פונקציה בdll המוגדרת בגדול מאוד בחתימה כזו:
void GetBuffer(type_t * in, const char **buffer, const unsigned int * length);
אני מקבל struct שמכיל מידע ומחזיר בתמורה buffer של בתים (במקרה הזה).
בGo יש ספרייה בשם unsafe אשר נועדה למצבים שבהם אני צריך לתרגם מצביע Go למצביע "מערכת" (כדוגמת C) למשל.
היא מאוד לא מומלצת לשימוש כלשהו, כי כמו שהשם מרמז, היא לא בטוחה והיא תלוית התנהגות של מערכת ושוברת את כל הבטיחות ששפת Go מספקת לשפה.
הבעיה היא שבזמן טעינת DLL בזמן ריצה בצורה דינאמית עם LazyDLL (למשל) צריך להעביר פרמטרים וזה דורש מאיתנו להשתמש ב unsafe.Pointer בשילוב של טיפוס בשם uintptr.
יש הרבה תיעוד בנושא של unsafe.Pointer וגם המון פוסטים ודוגמאות ברשת, אבל רובם מסובכים מידי ולא תמיד "נכונות" לכל מצב.
בהתחלה כאשר ניסיתי לקבל את buffer
חזרה, עבדתי לפי התיעוד.
קיבלתי באמת מידע בינארי, אבל כאשר שמרתי אותו לקובץ ופתחתי אותו עם hex editor גיליתי הרבה "זבל" כמו תוכן זיכרון כללי של התוכנית שלי ולא התוכן שציפיתי לקבל.
אחרי יום שלם כמעט של האבקות חופשית החלטתי להתעלם מכל מה שקראתי וראיתי אצל אחרים וחזרתי למקורות הפשוטים ביותר:
...
outBuffer := make([]byte, 2 << 24)
var length uint
...
getBufferFunc.Call(
uintptr(unsafe.Pointer(&MyType)),
uintptr(unsafe.Pointer(&outBuffer)),
uintptr(unsafe.Pointer(&length)),
)
...
saveToFile(outBuffer[:length])
...
בעצם "נזכרתי" כי מערך בC יכול להיות בעצם מצביע אשר אם אני מקבל את האורך שלו, אני יכול לקרוא רק את מה שאני צריך ולא צריך מעבר.
כלומר במקום הצעות להמיר את הslice לעוד מצביע עם uintptr ואז לבצע casting, חזרתי לחשיבה הפשוטה שדווקא היא עבדה וקיבלתי את התוכן הנכון.
הכנסתי לdll דגל אשר במידה ומדובר בגרסת פיתוח הוא גם כותב את buffer בעצמו לקובץ, וכך גם ידעתי להשוות בין הדברים (היות וקומפיילר של Go אינו יודע ליצור debug symbols בWindows) בצורה "טיפשה". וזאת לאחר ש delve לא הסכים לעבוד לי.
בקוד רגיל, ולא בהדגמה שכזו, חשוב גם לבדוק שגודל length
אינו גדול יותר 2 << 24
לשם כך אפשר לבדוק את ה cap
של ה slice
ואין צורך לחשב מחדש.
בפוסט הבא בנושא אסביר כיצד להתמודד עם struct שלם בתוך DLL.