לדלג לתוכן

המעטפת וחיי המחשב

המשתמש מעתיק טקסט, גורר קובץ לחלון, סוגר את המכסה ופותח אותו שעה אחר כך. אלה פעולות רגילות מבחינתו, אבל מבחינת התוכנית הן כמה גבולות שונים: בעלות על זיכרון, אמון בקלט, חיי אובייקטי COM ומשמעות הזמן שעבר.

בשיעור הזה נבחן את הגבולות האלה דרך פעולות קטנות. המטרה היא שהתוכנית תמשיך להתנהג נכון גם כשהמחשב אינו נשאר במצב שבו התחלנו את הניסוי.

הלוח הוא משאב משותף

הclipboard יכול להכיל כמה פורמטים לאותו תוכן. טקסט Unicode, תמונה ורשימת קבצים אינם אותו מבנה. קודם בוחרים פורמט שאנו יודעים לפרש, ואז פותחים את הלוח לזמן קצר, מעתיקים נתונים בבעלותנו וסוגרים.

בכתיבה של CF_UNICODETEXT משתמשים בזיכרון גלובלי מתאים. אחרי הצלחת SetClipboardData, הבעלות על האחסון עוברת למערכת. בכשל היא נשארת אצלנו. לעומת זאת, handle שהתקבל מGetClipboardData מושאל; לא משחררים אותו כאילו הקצנו אותו.

הדוגמה הבאה מציגה את המעבר הזה בכתיבה בלבד. הפעלתה מחליפה את תוכן הלוח בטקסט מעבדה, ולכן נריץ אותה כאשר זהו התוכן שאנחנו רוצים להעתיק. שמרו כcopy_text.cpp. היא אינה קוראת את תוכן הלוח הקודם.

#include <windows.h>
#include <cstring>
#include <cstdio>
#include <stdexcept>

class GlobalBlock {
    HGLOBAL value_;
public:
    explicit GlobalBlock(SIZE_T bytes) : value_(GlobalAlloc(GMEM_MOVEABLE, bytes)) {
        if (!value_) throw std::runtime_error("GlobalAlloc failed");
    }
    ~GlobalBlock() { if (value_) GlobalFree(value_); }
    GlobalBlock(const GlobalBlock&) = delete;
    GlobalBlock& operator=(const GlobalBlock&) = delete;
    HGLOBAL get() const { return value_; }
    void transferred() { value_ = nullptr; }
};

class ClipboardScope {
public:
    explicit ClipboardScope(HWND owner) {
        if (!OpenClipboard(owner)) throw std::runtime_error("clipboard busy or unavailable");
    }
    ~ClipboardScope() { CloseClipboard(); }
    ClipboardScope(const ClipboardScope&) = delete;
    ClipboardScope& operator=(const ClipboardScope&) = delete;
};

void copy_text(HWND owner) {
    const wchar_t text[] = L"AmitTech clipboard experiment";
    GlobalBlock block(sizeof(text));
    void* memory = GlobalLock(block.get());
    if (!memory) throw std::runtime_error("GlobalLock failed");
    std::memcpy(memory, text, sizeof(text));
    GlobalUnlock(block.get());
    ClipboardScope clipboard(owner);
    if (!EmptyClipboard()) throw std::runtime_error("EmptyClipboard failed");
    if (!SetClipboardData(CF_UNICODETEXT, block.get()))
        throw std::runtime_error("SetClipboardData failed");
    block.transferred();
}

int main() {
    HWND owner = CreateWindowExW(0, L"STATIC", L"AmitTech clipboard owner", 0,
        0, 0, 0, 0, HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr);
    if (!owner) return 1;
    int result = 0;
    try {
        copy_text(owner);
        std::puts("lab text copied");
    } catch (const std::exception& error) {
        std::fprintf(stderr, "%s\n", error.what());
        result = 2;
    }
    DestroyWindow(owner);
    return result;
}
cl /std:c++20 /EHsc /W4 copy_text.cpp user32.lib
copy_text.exe

החלון כאן הוא message-only window קטן שמספק owner תקין. העברת null לOpenClipboard ואז EmptyClipboard יכולה להשאיר owner ריק ולגרום לכתיבה להיכשל. הנקודה מתועדת בחוזה פתיחת הלוח.

ההקצאה והעתקת הטקסט מתבצעות לפני פתיחת הלוח, כדי להחזיק אותו לזמן קצר. עדיין אין כאן טרנזקציה: אם EmptyClipboard הצליחה וSetClipboardData נכשלה, התוכן הקודם כבר הוסר. זוהי משמעות כשל שצריך להכיר ולא להסתיר מאחורי RAII.

פענוח תוכן שמישהו אחר יצר

בקריאה של טקסט נבדוק שהפורמט קיים, שהגודל מוגבל ושקיים terminator בתוך האחסון. לא נקרא wcslen על buffer לא מהימן לפני בדיקת הגבול. את התוכן נעתיק לstd::wstring בזמן שהhandle נעול והלוח פתוח, ואז נשחרר את הנעילה ואת הלוח.

המערכת תומכת גם בdelayed rendering: בעל הלוח יכול להצהיר על פורמט ולייצר את התוכן רק כשמישהו מבקש אותו. המשמעות היא שקריאת clipboard יכולה לגרום לעבודה בצד אחר. יש גם כללים מיוחדים לטיפול בWM_RENDERFORMAT ובWM_RENDERALLFORMATS, ולכן לא מעתיקים את קוד הפתיחה הרגיל לתוך כל handler. את פעולות הלוח נקרא יחד עם תרשים חיי הowner.

גרירה היא פרוטוקול של COM

בdrag-and-drop נאתחל OLE בSTA באמצעות OleInitialize, נרשום IDropTarget עם RegisterDragDrop ונשמור message loop פעיל. נבטל את הרישום עם RevokeDragDrop לפני סוף חיי החלון ונאזן אתחול מוצלח עם OleUninitialize.

המקור מספק IDataObject, והיעד בודק פורמטים בעזרת FORMATETC. תוצאת GetData מגיעה בSTGMEDIUM שצריך לשחרר עם ReleaseStgMedium, משום שהבעלות יכולה להיות מוגדרת גם דרך אובייקט משחרר. לא כל medium הוא HGLOBAL שאפשר לשחרר ישירות.

גם רשימת קבצים אינה הרשאה לבצע בהם פעולה. נתיב יכול להצביע לreparse point או להשתנות לפני פתיחה. נעתיק את המידע הנחוץ, נפתח לפי מדיניות השירות ונאמת את האובייקט שעליו באמת נפעל. באותו זמן נזכור שמקור shell יכול לספק קובץ וירטואלי דרך stream, בלי נתיב מקומי רגיל. הסקירה של העברת אובייקטי shell מסבירה את השיחה בין המקור ליעד.

חלונית בחירה אינה parser של נתיבים

בבחירת קובץ נעדיף IFileOpenDialog ונקבל IShellItem. אם המוצר דורש קובץ במערכת הקבצים, נגדיר את הדרישה ונבקש SIGDN_FILESYSPATH. לא נניח שכל item של shell הוא pathname שאפשר להעביר לCreateFile.

החלונית היא קריאת COM ויכולה להפעיל לולאת הודעות פנימית. לא נחזיק lock של מצב היישום לאורך Show, ולא נניח שהחלון הראשי נשאר באותו state במשך כל הבחירה. ביטול של המשתמש הוא תוצאה צפויה, לא קריסה שצריך לדווח עליה כתקלה במערכת.

איזה זמן עבר בזמן שינה

זמן קיר עונה על השאלה מה התאריך והשעה. הוא יכול להשתנות בגלל סנכרון או שינוי ידני, ולכן אינו תמיד מתאים למדידת משך פעולה. שעון מונוטוני עוזר למדוד הפרשים, אבל צריך עדיין לשאול אם הוא כולל זמן שבו המחשב ישן.

בWin32, הקריאה GetTickCount64 כוללת זמן sleep וhibernation. הקריאה QueryUnbiasedInterruptTime סופרת זמן עבודה של המערכת ואינה כוללת אותם. אלה שני חוזים שונים, המתוארים בתיעוד הזמן של Windows. לא נסיק מהשם של API מנוהל בגרסת runtime אחרת שהוא משתמש תמיד באותו שעון.

נניח שנתנו למשתמש חלון של עשר דקות לאישור פעולה. בדרך כלל נרצה שגם שעה בשינה תפקיע אותו. לעומת זאת, תקציב של שתי שניות זמן מערכת פעיל לצורך ניסוי יכול לבחור שעון אחר. הבחירה צריכה להיות חלק מחוזה המוצר, לפני שבוחרים פונקציית timer.

תזכורת לבצע בדיקה אינה הוכחה לזמן

הודעות WM_TIMER אינן הבטחה שהhandler ירוץ בדיוק במועד. הן יכולות להתעכב כשהUI עסוק. לכן נשמור deadline לפי השעון המתאים, ובכל יקיצה נבדוק את הזמן הנוכחי. לא נגדיל מונה באחד ונניח שעברה בדיוק שנייה.

בחזרה משינה נבדוק מחדש תלויות חיצוניות: חיבור רשת יכול להיות שבור, תצוגה יכולה להשתנות ומדיניות יכולה להתעדכן. אפשר לטפל בהודעות power מתאימות ולרשום notifications נדרשות, אבל הhandler עצמו צריך להיות קצר. הוא ייזום בדיקה או שינוי state, ולא יבצע שחזור רשת ארוך בתוך callback של החלון.

המערכת צריכה להתמודד גם עם שינוי שהתרחש בלי notification שציפינו לה. הודעה היא הזדמנות לרענן מידע; היא אינה נעילה על מצב העולם עד ההודעה הבאה.