לדלג לתוכן

בעלות בין מנועי ריצה

ה־GC יודע שה־wrapper שלנו עדיין חי, אבל הוא אינו יודע אם ה־HANDLE שבתוכו כבר נסגר. מנגד handle פתוח יכול להישאר תקף גם אחרי שאיבדנו כל reference ל־wrapper. נקשור את שני העולמות באמצעות SafeHandle, ונבדיל בין הגנת משאב במהלך קריאה לבין השארת pointer בידיים של קוד טבעי אחרי החזרה.

הדוגמה הבאה יוצרת event טבעי, מסמנת אותו ומשחררת אותו דרך wrapper. היא מיועדת לווינדוס ודורשת AllowUnsafeBlocks בשביל LibraryImport. השימוש ב־int עבור תוצאות BOOL שומר ייצוג מפורש ופשוט.

using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;

using var handle = Native.CreateEvent(0, 1, 0, 0);
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastPInvokeError());
if (Native.SetEvent(handle) == 0)
    throw new Win32Exception(Marshal.GetLastPInvokeError());
Console.WriteLine("event signaled");

sealed class EventHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    public EventHandle() : base(ownsHandle: true) { }
    protected override bool ReleaseHandle() => Native.CloseHandle(handle) != 0;
}

internal static partial class Native
{
    [LibraryImport("kernel32.dll", EntryPoint = "CreateEventW", SetLastError = true)]
    internal static partial EventHandle CreateEvent(nint attributes, int manualReset,
        int initialState, nint name);
    [LibraryImport("kernel32.dll", SetLastError = true)]
    internal static partial int SetEvent(EventHandle handle);
    [LibraryImport("kernel32.dll", SetLastError = true)]
    internal static partial int CloseHandle(nint handle);
}

ה־marshaller של SafeHandle מגן על המשאב במהלך הקריאה המתאימה, כך ששחרור מקביל של ה־wrapper אינו סוגר את ה־handle באמצע השימוש של אותה קריאה. ההגנה אינה נמשכת אוטומטית אחרי החזרה מ־P/Invoke. אם הספרייה שומרת את ה־handle עבור פעולה אסינכרונית, נצטרך להאריך את תקופת הבעלות עד שהספרייה מאשרת שאינה משתמשת בו עוד.

ה־ReleaseHandle צריך להיות קטן, אמין וללא זריקת exceptions. הוא יכול להתבצע גם במסלול finalization, ולכן לא נבנה בו תלות ב־UI או במצב מנוהל שכבר נסגר. הקריאה Dispose מסמנת שחרור דטרמיניסטי, והמסלול הסופי מספק רשת נוספת למקרה שנשכח השחרור. זאת אינה הצדקה להשאיר handles פתוחים עד שה־GC יחליט לפעול.

ה־DangerousGetHandle מחזיר את המספר הגולמי בלי ליצור בעצמו השאלה מוגנת. כשאין דרך להעביר SafeHandle ישירות ל־API, אפשר להשתמש ב־DangerousAddRef וב־DangerousRelease סביב כל תחום השימוש, עם דגל שמציין אם ההגדלה הצליחה. צריך לבדוק גם את תוצאת הרכישה ולהבטיח Release בתוך finally. עצם שמירת SafeHandle בשדה מונעת איסוף, אבל אינה מונעת ממישהו לקרוא Dispose עליו.

ה־GCHandle מסוג Normal מחזיק object מנוהל בחיים ויכול לשמש כ־context token אטום עבור ספרייה טבעית. ה־IntPtr שמחזירים מ־GCHandle.ToIntPtr אינו כתובת של האובייקט ואין להעביר אותו ל־memcpy. ה־callback מחזיר אותו ל־GCHandle ומגיע ל־Target דרך ה־runtime. החוזה דורש שלא נבצע Free כל עוד callback יכול להגיע, ושלא נבצע Free פעמיים על עותקים של אותו handle.

ה־GCHandle מסוג Pinned מוסיף יציבות כתובת עבור אובייקט מתאים, במחיר השפעה אפשרית על ה־GC. גם כאן צריך להבדיל בין שחרור הקיבוע לבין שחרור המשאב הלוגי. ה־GC.KeepAlive משאיר reference חי עד נקודה אך אינו מקבע ואינו מוסיף reference count ל־HANDLE. כל אחד מן הכלים עונה על שאלה אחרת; הוספת כולם לאותו קוד בלי חוזה אינה יוצרת בטיחות.

בתחקיר נסמן לכל pointer או handle מי הבעלים, מי שואל, האם ייתכן Dispose מקביל ואיזה אירוע מסיים את ההשאלה. התיעוד על SafeHandle ועל GCHandle מפרט את המנגנונים. בשיעור הבא נשתמש בהם עבור callback שיכול לרוץ אחרי שהקריאה המקורית כבר חזרה.