Objects Structure: Under The Hood
אובייקטים בדוט-נט אינם בנויים בתור יחידות בודדת ושלמות שנמצאות על ה-GC Heap, שמכילות את ה-Data Member'ים שהגדרנו להן.
בפועל,
מעבר למידע "הנראה לעין" שאנחנו מגדירים כשאנחנו כותבים מחלקה חדשה,
מתווספים גם מספר שדות "בלתי נראים" לאובייקט שלנו שנמצאים באזורים שונים
בזכרון.
השדה הראשון והבסיסי הוא המצביע ל-MethodTable השייך לטיפוס שלנו. אני מדלג על ההסבר על ה-MethodTable והמידע שהיא מכילה (שימו לב שלא מדובר ב-VTable עצמו, אלא בטיפוס שבין היתר מכיל מצביע אליה), ובמקום זה אתמקד בנקודה אחרת:
מיקום המצביע.
העניין
הוא כזה, כשאנחנו יוצרים מופע של מחלקה, יש לנו גישה אך ורק ל-Member'ים
שהגדרנו. כלומר, שדות "נסתרים" שמתווספים אוטומטית לאובייקט שלנו נמצאים
מעבר להישג ידנו. ועם זאת, עדיין חלקם מוסתרים "פחות" ומוסתרים "יותר".
אני אסביר למה הכוונה..
מאחר ואנחנו נוגעים כאן בנושאים שנמצאים
"בברזלים" של הפלטפורמה, מבט בקוד ה-IL של המחלקה Object למשל לא יעזור
לנו מאחר ורוב המידע "המעניין" נסתר מאיתנו שם. במקום זה, נציץ בקוד של
SSCLI על מנת לקבל הבנה קצת יותר טובה על מה שקורה מאחורי הקלעים.
קובץ ה-Header של Object נמצא תחת הכתובת clr/src/vm/object.h. כשנפתח אותו, נראה את הקוד הבא:
class Object
{
protected:
MethodTable* m_pMethTab;
// ... lots of code
}
ממה שאנחנו רואים כאן, אפשר להתרשם שלאובייקט שלנו
מתווסף "שדה נסתר" אחד בלבד, שמצביע ל-VTable שמשוייך לטיפוס. אבל בפועל,
המצב מעט שונה.
בנוסף ל-VPointer שהתווסף לנו לתוך תחום האובייקט שלנו,
מתווסף לנו אוטומטית גם ה-ObjHeader. הסיבה שאנחנו לא רואים כל אזכור לו
בהגדרה של מחלקת Object היא שהוא לא נמצא בתוכה, אלא ב-Offset שלילי
(דהיינו, ממוקם לפניה בזכרון).
ה-ObjHeader בנוי מ-DWORD שמחזיק בתוכו
מספר ערכים שונים, שכוללים בין היתר את אינדקס ה-SyncBlock שמשוייך אליו
(יפורט בפוסט עתידי), מספר ה-AppDomain בו האובייקט קיים, ערך ה-Hashcode
של האובייקט, וביט נוסף לשימושי SpinLock שנעשים על האובייקט.
מאחר
ולכל ביט וביט יש משמעות שונה כאן, העבודה מול ה-ObjHeader נעשית על
הביטים עצמם, דרך מניפולציות שונות של Bit Mask'ים כאלו ואחרים. אפשר לקחת
בתור דוגמה את הפונקציה GetHeaderSyncBlockIndex שמיצאת את אינדקס
ה-SyncBlock מתוך ה-ObjHeader:
// Access to the Sync Block Index, by masking the Value.
DWORD GetHeaderSyncBlockIndex()
{
// pull the value out before checking it to avoid race condition
DWORD value = m_SyncBlockValue;
if ((value & (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE))
!= BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
{
return 0;
}
return value & MASK_SYNCBLOCKINDEX;
}
רק לשם ההבהרה, שימו לב שהקוד הזה אינו נמצא תחת מחלקת Object, אלא תחת המחלקה ObjHeader.
ההתיחסות
היחידה ל-ObjHeader מתוך המחלקה Object נעשית כאשר רוצים לקבל את
ה-ObjHeader, שמיוחס לאותו אובייקט. מדובר בפונקציית Getter פשוטה שפונה
ל-Offset המתאים ומחזירה את אותו ObjHeader.
ObjHeader *GetHeader()
{
return PTR_ObjHeader(PTR_HOST_TO_TADDR(this) - sizeof(ObjHeader));
}
לסיכום,ניתן לייצג את מבנה האובייקט בזכרון בצורה הבאה: