מה זה COM — למה התכנון של COM ב-Windows עדיין יפה
· עודכן בתאריך: · Go Komura · COM, ActiveX, פיתוח Windows
מה זה COM?
COM (Component Object Model) הוא “חוזה בינארי” לתקשורת בין רכיבים ב-Windows. זהו מנגנון שבו התקשורת מתבצעת דרך חוזה מחמיר הנקרא ממשק, החוצה הבדלי שפה ומהדר, וביסודו עומדת גישת התכנון “תכנתו מול חוזה, לא מול מימוש”.
מפת הידע של המאמר
COM הוא חוזה בינארי לתקשורת בין רכיבים ב-Windows, ו-IUnknown שכל הממשקים יורשים ממנו מספק בדיקת פונקציונליות באמצעות QueryInterface וניהול ספירת הפניות באמצעות AddRef/Release. זיהוי ייחודי של רכיבים וממשקים באמצעות GUID מסוג CLSID ו-IID מונע התנגשויות שמות, ותכנון שמוסיף ממשקים חדשים בלי לשנות ממשקים קיימים ובודק אותם עם QueryInterface מממש דו-קיום גרסאות. In-proc (שרת DLL), שנטען לאותו תהליך כמו הצד הקורא, מחייב התאמת מספר ביטים ונגרר בקריסת הצד השני, בעוד Out-of-proc (LocalServer) שרץ בתהליך נפרד חופשי מהמגבלה הזו אך מחייב היערכות לשגיאות ניתוק כמו RPC_E_DISCONNECTED כשהצד השני נעלם. ייצוג הצלחה/כישלון באמצעות ערך ההחזרה HRESULT, יחד עם Proxy/Stub והגדרת החוזה מראש ב-IDL, מנתקים את המנגנון כולו מגבולות השפה והתהליך.
flowchart LR
accTitle: מפת הידע של הסיבה שהתכנון של COM יפה
accDescr: תרשים המראה ש-COM, על בסיס ספירת הפניות דרך IUnknown ו-QueryInterface, וזיהוי באמצעות CLSID ו-IID, מממש את היתרונות של תאימות בינארית, הפרדת ממשקים, דו-קיום גרסאות, ושימוש חוזר החוצה גבולות תהליכים, ואת הקשר לצורות המיקום In-proc ו-Out-of-proc, ל-HRESULT, ולפעולה ההדדית עם .NET.
com["COM (Component Object Model)"]
iunknown["IUnknown"]
queryinterface["QueryInterface"]
com_reference_counting["ספירת הפניות ב-COM (AddRef/Release)"]
clsid["CLSID(Class ID)"]
iid["IID (מזהה ממשק)"]
hresult["HRESULT"]
com_binary_compatibility["תאימות בינארית של COM"]
com_interface_versioning["דו-קיום גרסאות ב-COM"]
in_proc_com["In-proc COM (שרת DLL)"]
com_localserver["COM LocalServer (שרת COM בתהליך נפרד)"]
bitness_match_requirement["דרישת התאמת bitness"]
in_proc_crash_propagation["גרירת קריסה ב-in-proc"]
out_of_proc_com_disconnection_error["שגיאת ניתוק ב-COM חוץ-תהליכי"]
proxy_stub["Proxy/Stub"]
idl["IDL (שפת הגדרת ממשקים)"]
dotnet_com_interop["פעולה הדדית עם COM ב-.NET (COM Interop)"]
progid["ProgID(Programmatic Identifier)"]
activex["ActiveX"]
com -->|"משתמש ב"| iunknown
com -->|"משתמש ב"| queryinterface
com -->|"משתמש ב"| com_reference_counting
com -->|"משתמש ב"| clsid
com -->|"משתמש ב"| iid
com -->|"משתמש ב"| hresult
com -->|"מממש את"| com_binary_compatibility
com_binary_compatibility -->|"מחייב"| iunknown
com -->|"מממש את"| com_interface_versioning
com_interface_versioning -->|"מחייב"| queryinterface
com -.->|"משתמש ב"| in_proc_com
com -.->|"משתמש ב"| com_localserver
in_proc_com -->|"מחייב"| bitness_match_requirement
in_proc_com -->|"עלול לגרום ל"| in_proc_crash_propagation
com_localserver -->|"מונע"| in_proc_crash_propagation
com_localserver -.->|"עלול לגרום ל"| out_of_proc_com_disconnection_error
com -.->|"משתמש ב"| proxy_stub
com -->|"משתמש ב"| idl
dotnet_com_interop -.->|"משתמש ב"| progid
dotnet_com_interop -->|"משתמש ב"| queryinterface
dotnet_com_interop -->|"משתמש ב"| hresult
dotnet_com_interop -->|"משתמש ב"| com_reference_counting
activex -->|"מחייב"| com
בתרשים, קו מלא מציין קשר שמתקיים תמיד וקו מקווקו מציין קשר מותנה (תנאי ההתקיימות מפורטים בהסבר של כל קשר בעמוד המפורט). רשימת כל הקשרים (סך הכול 23, עם אסמכתה ורמת ודאות) והגדרות המושגים המרכזיים מרוכזות בעמוד המפורט של מפת הידע (ביפנית). נתונים: JSON-LD / Turtle
שלושת המרכיבים החשובים של COM
מה שמפורט בפרק הזה הם הרכיבים שמהם בנוי המנגנון של COM. הפרק הבא, “ארבעת היתרונות של COM”, מתאר תכונות שמתקבלות כתוצאה משילוב הרכיבים האלה — כך שלא מדובר בהסבר כפול על אותו הדבר. הקשר בין השניים מוצג בטבלה בסוף פרק היתרונות.
1. תכנון ממוקד-ממשקים
ב-COM “החוזה קודם למימוש”. אפשר להשתמש באובייקט מבלי לדעת דבר על המימוש הפנימי שלו, כל עוד מכירים את הממשק שנחשף.
2. זיהוי באמצעות GUID(CLSID / IID)
לכל רכיב ולכל ממשק מוקצה מזהה ייחודי בעולם (GUID), כך שלא יכולה להתרחש התנגשות שמות מלכתחילה.
3. IUnknown
הממשק הבסיסי שכל ממשקי COM יורשים ממנו. הוא מספק שלוש פונקציות:
| שיטה | תפקיד |
|---|---|
QueryInterface |
שואלת אם קיים ממשק אחר |
AddRef |
מגדילה את ספירת ההפניות |
Release |
מקטינה את ספירת ההפניות (משמידה את עצמה כשהיא מגיעה לאפס) |
כששלושת המרכיבים האלה משולבים יחד, מה שנשאר בין הצד הקורא לבין המימוש הוא רק “ממשק המזוהה על ידי GUID, עם סדר שיטות קבוע”. לא השפה ולא המהדר מופיעים בחוזה הזה.
flowchart LR
accTitle: החוזה הבינארי של COM
accDescr: תרשים המראה שהצד הקורא, בכל שפה שהיא, וצד המימוש, בכל שפה שהיא, שניהם מתחברים רק לחוזה הקבוע בבינארי, כך שאין צורך לבנות מחדש את הצד השני כשמחליפים אחד מהם.
subgraph CALLER["הצד הקורא — כל שפה"]
A1["אפליקציית C++"]
A2["אפליקציית C#"]
A3["VBA / Python וכו׳"]
end
CONTRACT["החוזה(קבוע בבינארי)"]
CONTRACT -.-> c1["IID(GUID)קובע איזה חוזה"]
CONTRACT -.-> c2["סדר השיטות מ-IUnknown"]
CONTRACT -.-> c3["טיפוסי ארגומנטים וקריאה"]
subgraph IMPL["המימוש — כל שפה"]
B1["רכיב שנכתב ב-C++"]
B2["רכיב שנכתב ב-C#"]
end
A1 --> CONTRACT
A2 --> CONTRACT
A3 --> CONTRACT
CONTRACT --> B1
CONTRACT --> B2
איור 1: החוזה הבינארי של COM — לא שפת הצד הקורא ולא שפת המימוש מופיעות בחוזה, כך שאפשר להחליף כל צד בלי לבנות מחדש את האחר.
“תכנתו מול חוזה” — בקוד
כדי שלא יישאר רק כמושג מופשט בטקסט, נראה קוד מינימלי. זו דוגמה לשימוש ברכיב ICalcService, שרק מבצע חיבור, בלי לדעת דבר על המימוש שלו.
נתחיל מ-C++ (COM גולמי). ההגדרה של ICalcService שכתובה כאן היא החוזה עצמו, ואין שום מקום בקוד של הצד הקורא שמגלה אם המימוש נכתב ב-C++ או ב-C#.
#include <objbase.h>
// הגדרת החוזה. בדרך כלל כותרת בצורה הזו נוצרת מ-IDL
// בגלל הירושה מ-IUnknown, יש תמיד QueryInterface / AddRef / Release
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001"))
ICalcService : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(int a, int b, int* result) = 0;
};
// חוזה גרסת ההרחבה שנוספה מאוחר יותר. ה-GUID שונה
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};
// ה-CLSID של רכיב המימוש. בדרך כלל מוגדר בכותרת שנוצרת מ-IDL
static const CLSID CLSID_CalcService =
{ 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };
// הצד הקורא
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) { return hr; }
ICalcService* calc = nullptr;
hr = CoCreateInstance(CLSID_CalcService, nullptr, CLSCTX_ALL,
__uuidof(ICalcService), reinterpret_cast<void**>(&calc));
// המצביע שמוחזר כאן כבר עבר AddRef (ספירת ההפניות היא 1)
if (SUCCEEDED(hr))
{
int sum = 0;
hr = calc->Add(1, 2, &sum); // ניתן לקרוא בלי לדעת על המימוש
// שאלה בזמן ריצה "האם יש גם את חוזה גרסת ההרחבה" = מימוש של דו-קיום גרסאות
ICalcServiceEx* calcEx = nullptr;
if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
reinterpret_cast<void**>(&calcEx))))
{
// מגיעים לכאן רק אם זה רכיב מהגרסה החדשה
calcEx->Release(); // הצד המקבל הוא זה שמשחרר
}
// אם אין, מוחזר פשוט E_NOINTERFACE, וגם הגרסה הישנה ממשיכה לפעול
calc->Release(); // ספירת ההפניות מגיעה לאפס והאובייקט מושמד
}
CoUninitialize();
שלוש נקודות חשובות:
- כמעט אין מצב שבו כותבים
AddRefבעצמכם. גםCoCreateInstanceוגםQueryInterfaceכבר ביצעוAddRefעל המצביע שהם מחזירים. כלומר, העיקרון הוא ש”הצד שמקבל את המצביע קורא ל-Release”, וקוראים ל-AddRefבאופן מפורש רק כאשר מתחילים להחזיק את אותו מצביע במקום נוסף. - כישלון של
QueryInterfaceאינו חריגה. “אין לי את החוזה הזה” (E_NOINTERFACE) היא תשובה תקינה, וזה מה שמאפשר “להוסיף פונקציונליות חדשה בלי לשבור רכיבים ישנים”. - כל ערכי ההחזרה הם
HRESULT. החזרת הצלחה/כישלון כערך מוחזר, ולא כחריגה, היא הסכמה הכרחית כדי לחצות שפות. אופן זריקת החריגות שונה משפה לשפה, אך ערך מוחזר שלם יכול להתפרש על ידי כולם.
flowchart TB
accTitle: עקרון ספירת ההפניות ו-Release
accDescr: תרשים המראה ש-CoCreateInstance ו-QueryInterface מחזירים מצביע לאחר AddRef, כך שהצד המקבל קורא ל-Release בסיום השימוש, וכאשר ספירת ההפניות מגיעה לאפס האובייקט מושמד.
api["CoCreateInstance או QueryInterface"] --> ret["מוחזר מצביע לאחר AddRef"]
ret --> use["הצד המקבל משתמש בו"]
use --> rel["הצד המקבל קורא ל-Release"]
rel --> zero["ספירת הפניות אפס = השמדה"]
use -.->|"AddRef מפורש"| add["שמירת המצביע במקום נוסף"]
איור 2: הצד המקבל את המצביע הוא זה שקורא ל-Release, ו-AddRef נקרא במפורש רק כשמוסיפים מקום החזקה.
כך משתמשים באותו חוזה מ-C#. מכיוון שאותו GUID נחשב לאותו חוזה, לא משנה אם המימוש בפועל הוא ב-C++.
using System;
using System.Runtime.InteropServices;
// כותבים את אותו GUID כמו בצד ה-C++. זו ההצהרה על "אותו חוזה"
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
int Add(int a, int b);
}
// הצד הקורא
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
?? throw new InvalidOperationException("COMサーバーが登録されていません。");
object server = Activator.CreateInstance(t)!;
try
{
var calc = (ICalcService)server; // הטלת הטיפוס הזו מקבילה ל-QueryInterface
int sum = calc.Add(1, 2);
Console.WriteLine(sum); // 3
}
finally
{
Marshal.ReleaseComObject(server); // מקביל ל-Release
}
הסיבה שערך ההחזרה של Add בצד C# הוא int היא שהפעולה ההדדית של .NET עם COM מבצעת המרה קבועה: “הארגומנט האחרון מסוג [out, retval] הופך לערך ההחזרה, ו-HRESULT כושל מומר לחריגה”. גם AddRef/Release נעלמים מנקודת המבט של הצד הקורא, אבל הם לא נעלמו בפועל — עטיפה דקה שמספקת .NET (RCW) קוראת להם במקום. להשתמש באותו חוזה, בדרך הכתיבה הטבעית של כל שפה — זו הצורה האמיתית של “החוזה הבינארי”.
flowchart TB
accTitle: התאמה בין קוד C# ל-COM
accDescr: תרשים המראה שב-C# הטלת טיפוס לממשק מקבילה ל-QueryInterface, Marshal.ReleaseComObject מקביל ל-Release, HRESULT כושל מומר לחריגה, ו-AddRef ו-Release נקראים על ידי ה-RCW במקום הצד הקורא.
cs["קוד C#"] --> cast["הטלת טיפוס"]
cs --> rco["ReleaseComObject"]
cs --> hrx["HRESULT כושל"]
cast --> qi["מקביל ל-QueryInterface"]
rco --> rel["מקביל ל-Release"]
hrx --> ex["מומר לחריגה"]
cs -.-> rcw["ה-RCW מנהל את ספירת ההפניות"]
איור 3: גם באותו חוזה, בצד C# הוא מתורגם לדרך הכתיבה הטבעית של השפה, וההמרה מתבצעת על ידי ה-RCW ושכבת הפעולה ההדדית.
ארבעת היתרונות של COM
1. תאימות בינארית
רכיב שנבנה פעם אחת ניתן לשימוש חוזר ללא תלות בשפת התכנות או בזמן הריצה. אפשר, למשל, לקרוא לרכיב COM שנכתב ב-C++ מתוך C# או Python, וזה עובד באופן שגרתי.
2. הפרדת ממשקים
מכיוון שהמימוש מוסתר לחלוטין ורק החוזה נחשף, אפשר לשנות את המימוש הפנימי בחופשיות מבלי להשפיע על הצד הקורא.
3. דו-קיום גרסאות
כדי להוסיף פונקציונליות תוך שמירה על תאימות לאחור, העיקרון הבסיסי הוא להוסיף ממשקים חדשים. כך אפשר לספק פונקציונליות חדשה בלי לשנות את הממשקים הישנים.
כאשר משלבים צד קורא ישן/חדש עם רכיב ישן/חדש, שלושה מתוך ארבעת הצירופים פועלים כרגיל, והצירוף הרביעי מסתפק בתשובה של “אין את זה”.
flowchart LR
accTitle: תאימות בין גרסאות ישנות וחדשות
accDescr: תרשים המראה שצד קורא ישן שמכיר רק ICalcService מצליח לקרוא גם לרכיב הישן וגם לרכיב החדש, צד קורא חדש ששואל על ICalcServiceEx מצליח מול הרכיב החדש ומקבל E_NOINTERFACE מהרכיב הישן וממשיך לפעול עם הפונקציונליות הישנה.
OLDC["צד קורא ישן(מכיר רק ICalcService)"]
NEWC["צד קורא חדש(שואל גם על ICalcServiceEx)"]
OLDS["רכיב ישן(מיישם רק ICalcService)"]
NEWS["רכיב חדש(מיישם את שניהם)"]
OLDC -->|"QueryInterface(ICalcService) → S_OK"| OLDS
OLDC -->|"QueryInterface(ICalcService) → S_OK, לא נשבר"| NEWS
NEWC -->|"QueryInterface(ICalcServiceEx) → S_OK, פונקציונליות חדשה"| NEWS
NEWC -.->|"QueryInterface(ICalcServiceEx) → E_NOINTERFACE, ממשיך עם הישן"| OLDS
איור 4: הוספת IID חדש בלי לשנות ממשק קיים משאירה את כל צירופי הישן-חדש פועלים; הקו המקווקו הוא תשובה תקינה של “אין את החוזה הזה”.
4. שימוש חוזר החוצה גבולות תהליכים
לרכיבי COM יש שני מיקומי הרצה אפשריים: In-proc (שרת DLL), שנטען כ-DLL באותו תהליך כמו הצד הקורא, ו-Out-of-proc (שרת EXE, LocalServer), שמופעל כ-EXE בתהליך נפרד. המראה של קוד הקריאה זהה בשני המקרים.
flowchart TB
accTitle: שני מיקומי ההרצה של רכיב
accDescr: תרשים המראה שמאותו קוד קריאה נראה זהה, In-proc נטען כ-DLL באותו תהליך כמו הצד הקורא, ו-Out-of-proc מופעל כ-EXE בתהליך נפרד.
caller["קוד הקריאה(נראה זהה בשני המקרים)"] --> ip["In-proc(שרת DLL)"]
caller --> op["Out-of-proc(שרת EXE)"]
ip -.-> ipd["נטען כ-DLL באותו תהליך"]
op -.-> opd["מופעל כ-EXE בתהליך נפרד"]
איור 5: יש שני מיקומי הרצה, In-proc ו-Out-of-proc, אבל קוד הקריאה נראה זהה בשניהם.
| In-proc (שרת DLL) | Out-of-proc (שרת EXE) | |
|---|---|---|
| מיקום ההרצה | אותו תהליך כמו הצד הקורא | תהליך נפרד |
| מהות הקריאה | קריאה ישירה דרך מצביע לפונקציה | סידור מחדש של הארגומנטים (marshaling) והעברה דרך תקשורת בין-תהליכית |
| מהירות | מהיר | איטי יותר בגלל התקשורת הבין-תהליכית |
| כשהצד השני קורס | הצד הקורא נגרר איתו | הצד הקורא נשאר בחיים (הקריאה חוזרת ככישלון) |
| מספר ביטים (32/64bit) | חייב להתאים כדי להיטען | מותר שיהיה שונה |
עם Out-of-proc COM (שרת EXE) אפשר לקרוא בבטחה לפונקציונליות בתהליך נפרד. השורה האחרונה, “מותר ש-32bit/64bit יהיו שונים”, רלוונטית בפועל בשטח, ואת אופן הבנייה הקונקרטי של שימוש ב-DLL של 64bit מתוך אפליקציית 32bit נסביר במאמר “דוגמת גשר COM שקורא ל-DLL של 64bit מאפליקציית 32bit”.
עם זאת, העובדה שמדובר בתהליך נפרד גם אומרת שהצד השני עלול להיעלם בכל רגע. כשתהליך השרת קורס או מסתיים, הצד הקורא מקבל את הכשלים הבאים. אלה אינם “תקלה” אלא “הצד השני כבר לא קיים” — שגיאות ייחודיות ל-Out-of-proc.
| קוד שגיאה | משמעות |
|---|---|
RPC_E_DISCONNECTED |
האובייקט שרצינו לקרוא לו מנותק מהלקוח (האובייקט של הצד השני כבר לא קיים) |
RPC_S_SERVER_UNAVAILABLE |
אי אפשר להגיע לשרת (התהליך) של הקריאה |
ב-In-proc אין צורך לחשוב על זה כי “אם הוא נופל, גם אני נופל”, אבל ב-Out-of-proc זה נכנס לתכנון כטיפול בהתאוששות של הפעלה מחדש או חיבור מחדש. נכון יותר לחשוב על זה כעל עבודה נוספת שמתקבלת בתמורה לבטיחות.
flowchart TB
accTitle: מה קורה כשהתהליך השני נעלם
accDescr: תרשים המראה שב-Out-of-proc קריסה או סיום של תהליך השרת גורמים לקריאה לחזור ככישלון, הצד הקורא נשאר בחיים, ולכן צריך לתכנן טיפול של הפעלה מחדש או חיבור מחדש.
gone["קריסה או סיום של תהליך השרת"] --> fail["הקריאה חוזרת ככישלון"]
fail --> alive["הצד הקורא נשאר בחיים"]
alive --> rec["טיפול של הפעלה מחדש / חיבור מחדש"]
fail -.-> mean["לא 'נשבר' אלא 'הצד השני כבר לא קיים'"]
איור 6: ב-Out-of-proc הצד השני שנעלם לא גורר את הצד הקורא, אך בתמורה צריך לתכנן טיפול בהתאוששות.
הקשר בין שלושת המרכיבים לארבעת היתרונות
כפי שצוין בפתיחה, “שלושת המרכיבים” הם החלקים, ו”ארבעת היתרונות” הם התוצאה שלהם. כשמסדרים אילו חלקים תומכים באילו יתרונות, חלוקת התפקידים שנראתה חופפת מתבהרת.
| יתרון | המרכיב שמשפיע בעיקר |
|---|---|
| 1. תאימות בינארית | תכנון ממוקד-ממשקים + IUnknown (סדר הקריאה נקבע ברמת הבינארי) |
| 2. הפרדת ממשקים | תכנון ממוקד-ממשקים (חושף רק את החוזה) |
| 3. דו-קיום גרסאות | GUID + QueryInterface (אפשר לשאול בזמן ריצה על חוזה אחר) |
| 4. שימוש חוזר החוצה גבולות תהליכים | תכנון ממוקד-ממשקים (אפילו המיקום של המימוש מנותק מהחוזה) |
COM עדיין פעיל היום
נוטים לחשוב על COM כ”טכנולוגיה ישנה”, אבל הוא מנגנון שממשיך לשמש בליבת Windows גם היום.
היכן COM נמצא בשימוש
- הרחבות ל-Explorer (תפריטי קליק ימני, תצוגה מקדימה)
- אוטומציה של Office (שליטה חיצונית ב-Excel וב-Word)
- פעולה הדדית עם .NET (COM Interop)
- מערכות קיימות הכוללות ActiveX
- DirectX, Windows Shell API ועוד ה-API הרבים של Windows
גם אם נדמה ש”זה לא קשור אליי”, כל עוד מפתחים ל-Windows, COM צץ איפשהו.
סיכום
מרכז התכנון של COM הוא “עצמאות משפה, מתהליך וממימוש”. תכנון ממשקים ניטרלי-שפה, זיהוי ייחודי וניהול גרסאות באמצעות GUID, ספירת הפניות באמצעות IUnknown, ומנגנון שמטפל בתקשורת בין-תהליכית בצורה שקופה.
flowchart TB
accTitle: מרכז התכנון של COM
accDescr: תרשים המראה שתכנון ניטרלי-שפה של ממשקים, זיהוי ייחודי באמצעות GUID, ספירת הפניות של IUnknown, ותקשורת שקופה בין תהליכים - ארבעת המנגנונים האלה תומכים במרכז התכנון של COM - עצמאות משפה, מתהליך וממימוש.
i1["תכנון ניטרלי-שפה"] --> core["עצמאות משפה, תהליך ומימוש"]
i2["זיהוי ייחודי באמצעות GUID"] --> core
i3["ספירת הפניות של IUnknown"] --> core
i4["תקשורת שקופה בין תהליכים"] --> core
איור 7: ארבעת המנגנונים יחד תומכים במרכז התכנון של COM — עצמאות משפה, תהליך ומימוש.
“יפה” היא הגדרה סובייקטיבית, אבל הנימוקים להערכה הזו קונקרטיים. הנה הבעיות ש-COM ניסה לפתור, לצד הפתרונות שלו:
| מגבלה מאז (וגם היום) | הפתרון של COM |
|---|---|
| ל-C++ אין הסכם בינארי סטנדרטי (ABI), ולכן מהדר שונה בלבד מונע שימוש חוזר. גם ה-name mangling וגם פריסת האובייקטים אינם אחידים | הפכו רק את “סדר טבלת הפונקציות הווירטואליות” להסכם. בזכות הצמצום לנקודה הזו בלבד, השפה והמהדר הפכו ללא רלוונטיים |
| כשמחליפים ספרייה, צריך לבנות מחדש את כל מה שמשתמש בה | כל עוד החוזה (הממשק) לא משתנה, אין צורך בבנייה מחדש. אפשר להחליף במצב בינארי בלבד |
| רוצים להוסיף פונקציונליות, אבל אי אפשר לשבור את הצד הקורא הקיים | מוסיפים ממשק בלי לשנות את הקיים, ושואלים בזמן ריצה באמצעות QueryInterface האם הוא קיים |
| שמות מתנגשים (אותו שם מחלקה שייך לדברים שונים שקיימים זה לצד זה) | זיהוי ייחודי באמצעות GUID. כך תיאום השמות מיותר מלכתחילה |
| קריאה לפונקציונליות בתהליך אחר או במחשב אחר משנה לחלוטין את אופן הכתיבה של הקריאה | Proxy/Stub עומדים באמצע ושומרים על צורת הקוד של הצד הקורא זהה |
במילים אחרות, התכנון של COM לא התחיל מהשאלה “איך עושים את זה יפה”, אלא הגיע לצורתו הנוכחית כתוצאה מסילוק, אחד אחד, של המכשולים הקונקרטיים שמנעו שימוש חוזר. לא הרבה תכנונים מאפשרים לעקוב כל כך בפשטות אחר ההתאמה בין המגבלה לפתרון.
ודרך הפתרון הזו נשארה כמעט ללא שינוי בפיתוח מונחה-רכיבים המודרני.
| COM | המקבילה המודרנית |
|---|---|
| קובעים את החוזה מראש ב-IDL, ומייצרים ממנו קוד לשני הצדדים | מייצרים לקוח/שרת מסכימת OpenAPI או Protocol Buffers |
| לא משנים ממשק קיים, אלא מוסיפים ממשק חדש | ב-Protocol Buffers מוסיפים מספרי שדה חדשים בלי לעשות בהם שימוש חוזר, מחלקים API לגרסאות |
| ללא תלות בשפת המימוש (חוזה בינארי) | ללא תלות בשפת המימוש (חוזה הודעות דרך הרשת) |
| Proxy/Stub מסתירים את גבול התהליך | ה-stub של לקוח ה-RPC מסתיר את גבול הרשת |
ההבדל היחיד הוא סוג הגבול (גבול תהליך באותו מחשב, או רשת) ואופן ייצוג החוזה (בינארי, או טקסט/סכימה). מי שמבין את COM, כשהוא ניגש לתכנון ממשקים בין מיקרו-שירותים, מבין מעצמו — לא כמגמה חולפת אלא כהכרח — למה קובעים את החוזה מראש ולמה אסור למחוק שדות קיימים.
מאמרים קשורים
מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.
מה זה COM / ActiveX / OCX — הסבר מרוכז על ההבדלים והקשרים
מהו COM, מהו ActiveX ומהו OCX — סקירה מעשית של ההבדלים והקשרים ביניהם, הזיקה ל-OLE, איפה זה נמצא בשימוש, ואיך כדאי להתייחס לזה היום.
איך מתייחסים היום ל-ActiveX / OCX — טבלת החלטה: לשמר, לעטוף או להחליף
כשמוצאים ActiveX / OCX, מסודר כאן איך לבחור בין לשמר, לעטוף או להחליף — כולל 32bit / 64bit, רישום, תלות בדפדפן ותחזוקת ספקים.
מה זה Reg-Free COM - שימוש ב-COM בלי רישום
המאמר מסדר את היסודות של Reg-Free COM - תפקיד ה-activation context וה-manifest, היתרונות, המגבלות, וקריטריוני ההחלטה בפועל.
בניית פלט דוחות Excel - COM, Open XML, תבנית
פלט דוחות Excel משתנה מהותית לפי השאלה אם מפעילים את Excel אוטומטית, יוצרים xlsx ישירות, או משמרים VBA קיים. המאמר מסדר את קריטריוני הבחי...
מבוא ל-Media Foundation — מבינים את ה-API מנקודת המבט של COM
המאמר מסביר מה זה Media Foundation, יחד עם המונחים הבסיסיים של ה-API למדיה ב-Windows כמו COM, HRESULT, IMFSourceReader ו-MFT, בסדר שכדא...
נושאים קשורים
העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.
נושאים טכניים ב-Windows
שער לנושאי פיתוח Windows, חקירת תקלות וניצול נכסים קיימים.
העברת ActiveX
החלטות אם לשמר, לעטוף או להחליף רכיבי COM / ActiveX / OCX.
שירותים הקשורים לנושא הזה
המאמר קשור ישירות לשירותים הבאים.
שימוש חוזר והעברה של נכסים קיימים
הבנת התכנון והתאימות של COM היא נקודת כניסה טובה לחשיבה על איך למנף נכסי Windows קיימים.
ייעוץ טכני וסקירת תכנון
אם תרצו לגבש כיוון תוך התחשבות בזוויות הראייה של IUnknown, GUID ותכנון גבולות, זה מוביל לייעוץ טכני ולסקירת תכנון.
שאלות נפוצות
שאלות נפוצות בפניות בנושא המאמר.
- מה זה COM?
- COM (Component Object Model) הוא "חוזה בינארי" לתקשורת בין רכיבים ב-Windows. זהו מנגנון שבו התקשורת מתבצעת דרך חוזה מחמיר הנקרא ממשק, שחוצה הבדלי שפה ומהדר. בבסיסו עומדת גישת התכנון "תכנתו מול חוזה, לא מול מימוש".
- מה זה IUnknown?
- IUnknown הוא הממשק הבסיסי שכל ממשקי COM יורשים ממנו. הוא מספק שלוש פונקציות: QueryInterface ששואל האם קיים ממשק אחר, AddRef שמגדיל את ספירת ההפניות, ו-Release שמקטין את ספירת ההפניות ומשמיד את האובייקט כשהיא מגיעה לאפס. ניהול חיי האובייקטים ב-COM מבוסס כולו על ספירת ההפניות הזו.
- האם COM עדיין בשימוש היום?
- כן, COM הוא מנגנון שממשיך לשמש בליבת Windows גם היום. הוא מופיע במקומות רבים: הרחבות ל-Explorer (תפריטי קליק ימני ותצוגה מקדימה), אוטומציה של Office ב-Excel וב-Word, COM Interop מול .NET, מערכות קיימות שכוללות ActiveX, DirectX ו-Windows Shell API ועוד. כל עוד מפתחים ל-Windows, COM צץ איפשהו.
- מהם היתרונות של COM?
- יש ארבעה יתרונות עיקריים: תאימות בינארית שמאפשרת לעשות שימוש חוזר ברכיב שנבנה פעם אחת, ללא תלות בשפה או בזמן ריצה; הפרדת ממשקים שמסתירה את המימוש וחושפת רק את החוזה; דו-קיום גרסאות ששומר על תאימות לאחור באמצעות הוספת ממשקים חדשים; וקריאה בטוחה לפונקציונליות בתהליך נפרד באמצעות COM חוץ-תהליכי (שרת EXE).