אוטומציה וממשקי זמן ריצה¶
עד עכשיו הלקוח הכיר בזמן קומפילציה את סדר המתודות בממשק. אבל מה יקרה אם הלקוח הוא שפת scripting, או כלי שמקבל שם פעולה רק בזמן ריצה? הוא צריך לשאול על שם, להעביר ערכים עם תיאור טיפוס, ולקבל תוצאה בלי לבנות מראש class של C++ לכל רכיב.
המנגנון Automation נותן לנו שפה משותפת כזאת. הממשק IDispatch מאפשר לתרגם שם לDISPID ולהפעיל מתודה דרך Invoke. הערכים עוברים בדרך כלל בVARIANT. מאחורי הנוחות יש חוזי בעלות מדויקים, ונוח מאוד לטעות בהם אם מתייחסים לכל שדה כאל מספר שאפשר להעתיק.
מחרוזת אינה רק כתובת עד אפס¶
הטיפוס BSTR נראה בקוד כמו מצביע לתווים רחבים, אבל הוא שייך למשפחת הקצאות מוגדרת ומכיל אורך שמור. ניצור אותו עם SysAllocStringLen ונשחרר עם SysFreeString. לא נשתמש בdelete[], ולא נהפוך literal לBSTR רק בעזרת cast.
המחרוזת יכולה להכיל תו אפס באמצע. לכן wcslen וSysStringLen יכולים לתת תשובות שונות. האורך שמוחזר הוא מספר יחידות UTF-16, ולא בהכרח מספר התווים שהמשתמש רואה. את חוזה האורך נבחן בניסוי הבא.
מעבירים בעלות עם הטיפוס¶
הקובץ automation_values.cpp הוא תוכנית מלאה. הוא מראה מחרוזת עם אפס פנימי, העתקה של variant ומערך שהאינדקס הראשון שלו שלילי. אין צורך בactivation של שרת COM כדי לבצע את הפעולות המקומיות האלה.
#include <windows.h>
#include <oleauto.h>
#include <cstdio>
#include <cwchar>
#include <exception>
#include <stdexcept>
#include <new>
struct Variant {
VARIANT value;
Variant() { VariantInit(&value); }
Variant(const Variant&) = delete;
Variant& operator=(const Variant&) = delete;
~Variant() { if (FAILED(VariantClear(&value))) std::terminate(); }
};
void check(HRESULT hr) {
if (FAILED(hr)) throw std::runtime_error("Automation operation failed");
}
int main() {
try {
Variant text;
text.value.bstrVal = SysAllocStringLen(L"A\0B", 3);
if (!text.value.bstrVal) throw std::bad_alloc();
text.value.vt = VT_BSTR;
Variant copied;
check(VariantCopy(&copied.value, &text.value));
std::printf("stored=%u terminated=%zu separate=%d\n",
SysStringLen(text.value.bstrVal),
std::wcslen(text.value.bstrVal),
text.value.bstrVal != copied.value.bstrVal);
SAFEARRAYBOUND bound{3, -2};
Variant array;
array.value.parray = SafeArrayCreate(VT_I4, 1, &bound);
if (!array.value.parray) throw std::bad_alloc();
array.value.vt = VT_ARRAY | VT_I4;
for (LONG index = -2; index <= 0; ++index) {
LONG number = (index + 3) * 10;
check(SafeArrayPutElement(array.value.parray, &index, &number));
}
LONG low = 0, high = 0;
check(SafeArrayGetLBound(array.value.parray, 1, &low));
check(SafeArrayGetUBound(array.value.parray, 1, &high));
for (LONG index = low; index <= high; ++index) {
LONG number = 0;
check(SafeArrayGetElement(array.value.parray, &index, &number));
std::printf("index=%ld number=%ld\n", index, number);
}
LONG borrowed_number = 17;
Variant borrowed;
borrowed.value.vt = VT_BYREF | VT_I4;
borrowed.value.plVal = &borrowed_number;
check(VariantClear(&borrowed.value));
std::printf("borrowed still exists=%ld\n", borrowed_number);
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
הvariant הוא tagged union: השדה vt קובע איזה שדה פעיל ומה משמעותו. העתקה של כל המבנה עם השמה רגילה הייתה משכפלת את כתובת הBSTR, ואז שני בעלים היו מנסים לשחרר אותה. הפונקציה VariantCopy מבצעת את פעולת ההעתקה המתאימה לטיפוס. כאשר הערך מחזיק interface pointer, ההעתקה קשורה לreference counting ולא לשכפול אובייקט השרת.
הדגל VT_BYREF משנה את החוזה. כאן הvariant שואל כתובת של LONG מקומי. השחרור מנקה את העטיפה ואינו מוחק את המשתנה המושאל. העתקת variant כזה אינה מאריכה את חיי המשתנה. אם נשמור אותו אחרי חזרת הפונקציה, קיבלנו dangling pointer למרות שכל קריאות הAutomation הצליחו.
מערך עם גבולות אמיתיים¶
המבנה SAFEARRAY מחזיק מספר ממדים, גבולות ומידע שנחוץ לטיפול באיברים. לא מניחים שהאינדקס הראשון הוא אפס. לא מניחים גם שכל איבר הוא מספר פשוט: מערך BSTRים דורש טיפול במחרוזות, ומערך VARIANTים דורש טיפול בכל ערך לפי הטיפוס שלו.
גישה ישירה עם SafeArrayAccessData יכולה לחסוך קריאות לכל איבר, אבל היא נועלת את המערך עד SafeArrayUnaccessData. חייבים לשחרר את הגישה בכל מסלול כשל לפני הריסת המערך. הדוגמה משתמשת בפעולות איבר כדי שנוכל להתמקד קודם בבעלות. הguard שלנו מסיים את התהליך אם VariantClear נכשל, משום שבניסוי הזה אין סיבה תקינה להגיע למערך נעול בזמן הריסה; במוצר נגדיר מדיניות אבחון מתאימה ולא נסתיר את ההפרה.
מפעילים פעולה לפי שם¶
אחרי שקיבלנו IDispatch*, נשאל את GetIDsOfNames על שם הפעולה. אחר כך נכין DISPPARAMS, flags וסלוט לתוצאה. הprototype הבא הוא helper שמצטרף ללקוח קיים; הוא אינו יוצר שרת. החוזה של השרת הוא מתודה בשם Subtract שמקבלת שני ערכי LONG ומחזירה variant מספרי.
HRESULT invoke_subtract(IDispatch* object, LONG left, LONG right,
VARIANT* result, EXCEPINFO* exception, UINT* bad_arg) {
if (!object || !result || !exception || !bad_arg) return E_POINTER;
LPOLESTR name = const_cast<LPOLESTR>(L"Subtract");
DISPID id = DISPID_UNKNOWN;
HRESULT hr = object->GetIDsOfNames(IID_NULL, &name, 1,
LOCALE_INVARIANT, &id);
if (FAILED(hr)) return hr;
VARIANTARG args[2];
VariantInit(&args[0]);
VariantInit(&args[1]);
args[0].vt = VT_I4; args[0].lVal = right;
args[1].vt = VT_I4; args[1].lVal = left;
DISPPARAMS parameters{args, nullptr, 2, 0};
return object->Invoke(id, IID_NULL, LOCALE_INVARIANT, DISPATCH_METHOD,
¶meters, result, exception, bad_arg);
}
הארגומנטים המיקומיים מסודרים הפוך במערך, ולכן חיסור הוא ניסוי טוב יותר מחיבור כדי לגלות טעות בסדר. הלקוח יאתחל את result בVariantInit, יאפס את EXCEPINFO, ולאחר הקריאה ישחרר תוצאה ומחרוזות חריגה לפי החוזה. אם השרת החזיר callback לדחיית מילוי פרטי החריגה, נטפל בו לפני קריאת הפרטים. בהרצאה הבאה נשתמש בגבול שגיאה מפורש כדי שלא כל HRESULT יהפוך אוטומטית לretry.
בכתיבת property יש named argument מיוחד בשם DISPID_PROPERTYPUT; אי אפשר תמיד להשתמש באותה תבנית כמו קריאת מתודה. גם puArgErr מצביע לאינדקס במערך ההפוך, ורלוונטי רק לקודי שגיאה מתאימים. את הפרטים האלה נבדוק מול חוזה Invoke.
עוברים להיטל שפה¶
המנגנון WinRT משתמש בmetadata ובממשקים בינאריים, עם בסיס IInspectable שמרחיב את IUnknown. הprojection של C++/WinRT נותן תחביר נוח יותר, עטיפות בעלות ותרגום שגיאות. הוא אינו מבטל את apartment, את החיים של callback או את הצורך לבדוק תמיכת API במערכת היעד.
הדוגמה הבאה היא תוכנית נפרדת בשם uri.cpp, עם Windows SDK שמכיל את headers של C++/WinRT. היא מנתחת URI מקומי בזיכרון ואינה פותחת חיבור רשת.
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <cstdio>
int main() {
try {
winrt::init_apartment(winrt::apartment_type::multi_threaded);
} catch (const winrt::hresult_error& error) {
std::printf("init=%08lx\n", static_cast<unsigned long>(error.code().value));
return 1;
}
int exit_code = 0;
try {
winrt::Windows::Foundation::Uri uri{L"https://example.com:8443/course"};
auto host = uri.Host();
std::wprintf(L"host=%ls port=%d\n", host.c_str(), uri.Port());
} catch (const winrt::hresult_error& error) {
std::printf("operation=%08lx\n", static_cast<unsigned long>(error.code().value));
exit_code = 2;
} catch (...) {
exit_code = 3;
}
winrt::uninit_apartment();
return exit_code;
}
המשתנה uri נראה כמו אובייקט C++ רגיל, אבל הוא עטיפה לממשק של runtime class. העתקת עטיפה היא פעולת בעלות על reference; היא אינה בהכרח יצירת URI חדש בתוך המערכת. המחרוזת hstring אינה BSTR, ולכן גם אליה לא נחבר פונקציית שחרור ממשפחה אחרת.
נפתח את הגדרת Uri::Port בheader ונעקוב מהproperty הנוח עושה ברמת ABI. כך נוכל לראות כיצד HRESULT ופרמטר output הופכים לערך מוחזר ולחריגה בשפת C++. הדוגמה אינה דורשת לממש projection בעצמנו; היא דורשת להבין איזה חוזה הוא מסתיר. אפשר להיעזר בתיעוד הגישה לשכבת ABI.