מה זה COM — למה התכנון של Windows COM עדיין מחזיק

· עודכן בתאריך: · · COM, ActiveX, פיתוח Windows

היסטוריית עדכונים (גרסה ראשונה, פורסמה בתאריך 25 Jan 2026)
פרסום ראשון
לצטט את המאמר הזה(DOI: 10.5281/zenodo.22173242)

מאמר זה מאוחסן בארכיון Zenodo. להלן גם ה-DOI שתמיד מפנה לגרסה האחרונה וגם ה-DOI המקובע לגרסה שאתם קוראים.

Go Komura (2026). מה זה COM — למה התכנון של Windows COM עדיין מחזיק. KomuraSoft LLC. https://doi.org/10.5281/zenodo.22173242 https://comcomponent.com/he/blog/why-com-is-beautiful/

DOI (הגרסה האחרונה)
10.5281/zenodo.22173242
DOI (הגרסה הזו)
10.5281/zenodo.22173243

מה זה COM?

COM (Component Object Model) הוא binary contract לתקשורת בין components ב-Windows. זה מנגנון שבו מדברים דרך interface כחוזה קשיח, מעבר להבדלי שפה ו-compiler, וביסודו עומדת גישת התכנון “תכנתם מול החוזה, לא מול ה-implementation”.

ב-diagram, solid line מציינת relation שתמיד מתקיים ו-dashed line מציינת relation מותנה (התנאים מופיעים בהסבר של כל relation ב-detail page). הרשימה המלאה של ה-relations (סה”כ 23, כולל evidence ו-certainty) וההגדרות של ה-concepts המרכזיים נמצאות ב-detail page של ה-knowledge map (ביפנית). Data: JSON-LD / Turtle

שלושת החלקים של COM

הפרק הזה מתאר את החלקים שמהם בנוי COM. הפרק הבא, “ארבעת היתרונות של COM”, מתאר תכונות שמתקבלות כתוצאה משילוב החלקים האלה — לא הסבר כפול על אותו דבר. ההתאמה מופיעה בטבלה בסוף פרק היתרונות.

1. תכנון ממוקד-interfaces

ב-COM “החוזה קודם ל-implementation”. אפשר להשתמש באובייקט בלי לדעת כלום על ה-implementation הפנימי, כל עוד מכירים את ה-interface שנחשף.

2. זיהוי עם GUID (CLSID / IID)

לכל component ולכל interface יש ID ייחודי בעולם (GUID), כך ש-name collision לא קורה מלכתחילה.

3. IUnknown

ה-interface הבסיסי שכל COM interface יורש. הוא נותן שלוש פונקציות:

method תפקיד
QueryInterface שואל אם יש interface אחר
AddRef מעלה את ה-reference count
Release מוריד את ה-reference count (משמיד את עצמו כשמגיעים לאפס)

כששלושת החלקים האלה עובדים יחד, מה שנשאר בין ה-caller ל-implementation הוא רק “interface שמזוהה ב-GUID, עם סדר methods קבוע”. לא השפה ולא ה-compiler מופיעים בחוזה.

ה-implementation — השפה לא משנהה-caller — השפה לא משנהcomponent שנכתב ב-C++component שנכתב ב-C#אפליקציית C++אפליקציית C#VBA / Python וכו'החוזה (החלק הקבוע בבינארי)· IID (GUID) קובע איזה חוזה זה· סדר ה-methods שמתחיל מ-IUnknown· טיפוסי הארגומנטים, ערך ההחזרה ו-calling convention לכל method

איור 1: ה-binary contract של COM. לא שפת ה-caller ולא שפת ה-implementation מופיעות בחוזה, אז אפשר להחליף כל צד בלי לבנות מחדש את השני.

“תכנתם מול החוזה” — בקוד

כדי שזה לא יישאר מושג מופשט, הנה קוד מינימלי. דוגמה לשימוש ב-component ICalcService שרק מחבר מספרים, בלי לדעת כלום על ה-implementation.

נתחיל מ-C++ (raw COM). ההגדרה של ICalcService כאן היא החוזה עצמו. אין שום מקום בקוד של ה-caller שמגלה אם ה-implementation נכתב ב-C++ או ב-C#.

#include <objbase.h>

// הגדרת החוזה. בדרך כלל header בצורה הזו נוצר מ-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 של רכיב ה-implementation. בדרך כלל מוגדר ב-header שנוצר מ-IDL
static const CLSID CLSID_CalcService =
    { 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };

// ה-caller
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));
// ה-pointer שחוזר כאן כבר עבר AddRef (ה-reference count הוא 1)

if (SUCCEEDED(hr))
{
    int sum = 0;
    hr = calc->Add(1, 2, &sum);          // אפשר לקרוא בלי לדעת על ה-implementation

    // שאלה ב-runtime "יש גם את חוזה גרסת ההרחבה?" = מימוש של version coexistence
    ICalcServiceEx* calcEx = nullptr;
    if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
                                       reinterpret_cast<void**>(&calcEx))))
    {
        // מגיעים לכאן רק אם זה component מהגרסה החדשה
        calcEx->Release();               // הצד שקיבל הוא זה שעושה Release
    }
    // אם אין, חוזר פשוט E_NOINTERFACE, וגם הגרסה הישנה ממשיכה לעבוד

    calc->Release();                     // ה-reference count מגיע לאפס והאובייקט מושמד
}
CoUninitialize();

שלוש נקודות לקריאה:

  • כמעט אין מצב שבו כותבים AddRef בעצמכם. גם CoCreateInstance וגם QueryInterface כבר עשו AddRef על ה-pointer שהם מחזירים. כלומר העיקרון הוא “מי שקיבל את ה-pointer קורא ל-Release”, וקוראים ל-AddRef במפורש רק כשמתחילים להחזיק את אותו pointer במקום נוסף.
  • כישלון של QueryInterface אינו חריג. “אין לי את החוזה הזה” (E_NOINTERFACE) היא תשובה תקינה, וזה מה שמאפשר להוסיף פונקציונליות חדשה בלי לשבור components ישנים.
  • כל ערכי ההחזרה הם HRESULT. הצלחה/כישלון כערך מוחזר, לא כ-exception, היא הסכמה הכרחית כדי לחצות שפות. אופן זריקת exceptions שונה משפה לשפה, אבל integer return value כולם יודעים לפרש.
עקרון ה-reference count ו-ReleaseCoCreateInstance ו-QueryInterface מחזירים pointer אחרי AddRef, אז הצד שקיבל קורא ל-Release בסיום השימוש, וכשה-reference count מגיע לאפס האובייקט מושמד.AddRef מפורשCoCreateInstance או QueryInterfaceחוזר pointer אחרי AddRefהצד שקיבל משתמש בוהצד שקיבל קורא ל-Releasereference count אפס = השמדהשומרים את ה-pointer במקום נוסף

איור 2: מי שקיבל את ה-pointer הוא זה שקורא ל-Release. AddRef במפורש רק כשמוסיפים מקום החזקה.

כך משתמשים באותו חוזה מ-C#. אותו GUID נחשב לאותו חוזה, אז לא משנה אם ה-implementation בפועל הוא 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);
}

// ה-caller
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
         ?? throw new InvalidOperationException("שרת COM לא רשום.");
object server = Activator.CreateInstance(t)!;
try
{
    var calc = (ICalcService)server;   // ה-cast הזה מקביל ל-QueryInterface
    int sum = calc.Add(1, 2);
    Console.WriteLine(sum);            // 3
}
finally
{
    Marshal.ReleaseComObject(server);  // מקביל ל-Release
}

הסיבה שערך ההחזרה של Add בצד C# הוא int היא ש-COM Interop של .NET עושה המרה קבועה: “הארגומנט האחרון מסוג [out, retval] הופך לערך ההחזרה, ו-HRESULT שנכשל הופך ל-exception”. גם AddRef/Release נעלמים מנקודת המבט של ה-caller, אבל הם לא נעלמו — wrapper דק ש-.NET מספק (RCW) קורא להם במקום. להשתמש באותו חוזה, בצורה הטבעית של כל שפה — זו הצורה האמיתית של “binary contract”.

התאמה בין קוד C# ל-COMב-C# cast ל-interface מקביל ל-QueryInterface, Marshal.ReleaseComObject מקביל ל-Release, HRESULT שנכשל הופך ל-exception, ו-AddRef ו-Release נקראים על ידי ה-RCW.קוד C#cast לטיפוסReleaseComObjectHRESULT שנכשלמקביל ל-QueryInterfaceמקביל ל-Releaseהופך ל-exceptionה-RCW מנהל את ה-reference count

איור 3: גם באותו חוזה, בצד C# הוא מתורגם לצורה הטבעית של השפה, וההמרה נעשית על ידי ה-RCW ושכבת ה-interop.

ארבעת היתרונות של COM

1. Binary compatibility

רכיב שנבנה פעם אחת אפשר לעשות בו reuse בלי תלות בשפת התכנות או ב-runtime. לקרוא ל-COM component שנכתב ב-C++ מתוך C# או Python זה דבר שגרתי.

2. הפרדת interfaces

כי ה-implementation מוסתר לגמרי ורק החוזה נחשף, אפשר לשנות את ה-implementation הפנימי בחופשיות בלי להשפיע על ה-caller.

3. Version coexistence

כדי להוסיף פונקציונליות תוך שמירה על backward compatibility, העיקרון הבסיסי הוא להוסיף interfaces חדשים. כך מספקים פונקציונליות חדשה בלי לשנות את ה-interfaces הישנים.

כשמשלבים caller ישן/חדש עם component ישן/חדש, שלושה מתוך ארבעת הצירופים עובדים כרגיל, והצירוף הרביעי מסתפק בתשובה “אין את זה”.

QueryInterface(IID_ICalcService)S_OKQueryInterface(IID_ICalcService)S_OK — עדכון לא שוברQueryInterface(IID_ICalcServiceEx)S_OK — אפשר להשתמש בפונקציונליות חדשהQueryInterface(IID_ICalcServiceEx)E_NOINTERFACE — ממשיכים עם הישןcaller ישןמכיר רק ICalcServicecaller חדששואל גם על ICalcServiceExcomponent ישןמיישם רק ICalcServicecomponent חדשמיישם את שניהם

איור 4: הוספת IID חדש בלי לשנות interface קיים משאירה את כל צירופי הישן-חדש עובדים. הקו המקווקו הוא תשובה תקינה של “אין את החוזה הזה”.

4. Reuse מעבר לגבול process

ל-COM components יש שני מיקומי הרצה. In-proc (DLL server) נטען כ-DLL באותו process כמו ה-caller, ו-Out-of-proc (EXE server, LocalServer) מופעל כ-EXE ב-process נפרד. קוד הקריאה נראה זהה בשני המקרים.

שני מיקומי ההרצה של componentמאותו קוד קריאה שנראה זהה, In-proc נטען כ-DLL באותו process כמו ה-caller, ו-Out-of-proc מופעל כ-EXE ב-process נפרד.קוד הקריאה (נראה זהה בשני המקרים)In-proc (DLL server)Out-of-proc (EXE server)נטען כ-DLL באותו processמופעל כ-EXE ב-process נפרד

איור 5: יש שני מיקומי הרצה, In-proc ו-Out-of-proc, אבל קוד הקריאה נראה זהה בשניהם.

  In-proc (DLL server) Out-of-proc (EXE server)
איפה זה רץ אותו process כמו ה-caller process נפרד
מהות הקריאה קריאה ישירה דרך function pointer packing מחדש של הארגומנטים (marshaling) והעברה ב-IPC
מהירות מהיר איטי יותר בגלל ה-IPC
כשהצד השני קורס ה-caller נגרר איתו ה-caller נשאר בחיים (הקריאה חוזרת ככישלון)
bitness (32/64-bit) חייב להתאים כדי להיטען מותר שיהיה שונה

עם Out-of-proc COM (EXE server) אפשר לקרוא בבטחה לפונקציונליות ב-process נפרד. השורה האחרונה, “מותר ש-32-bit/64-bit יהיו שונים”, רלוונטית בשטח, ואת אופן הבנייה הקונקרטי של שימוש ב-DLL של 64-bit מתוך אפליקציית 32-bit נסביר במאמר “גשר COM: איך אפליקציית 32-bit קוראת ל-DLL של 64-bit”.

עם זאת, process נפרד גם אומר שהצד השני עלול להיעלם בכל רגע. כש-process השרת קורס או מסתיים, ה-caller מקבל את הכשלים הבאים. אלה לא “תקלה” אלא “הצד השני כבר לא קיים” — שגיאות ייחודיות ל-Out-of-proc.

קוד שגיאה משמעות
RPC_E_DISCONNECTED האובייקט שרצינו לקרוא לו מנותק מה-client (האובייקט של הצד השני כבר לא קיים)
RPC_S_SERVER_UNAVAILABLE אי אפשר להגיע לשרת (ה-process) של הקריאה

ב-In-proc אין צורך לחשוב על זה כי “אם הוא נופל, גם אני נופל”, אבל ב-Out-of-proc זה נכנס לתכנון כ-recovery של restart או reconnect. נכון יותר לחשוב על זה כעל עבודה נוספת שמתקבלת בתמורה לבטיחות.

מה קורה כשה-process השני נעלםב-Out-of-proc crash או סיום של process השרת גורמים לקריאה לחזור ככישלון, ה-caller נשאר בחיים, ולכן צריך לתכנן recovery של restart או reconnect.crash או סיום של process השרתהקריאה חוזרת ככישלוןה-caller נשאר בחייםrecovery של restart / reconnectלא 'נשבר' אלא 'הצד השני כבר לא קיים'

איור 6: ב-Out-of-proc הצד השני שנעלם לא גורר את ה-caller, אבל בתמורה צריך לתכנן recovery.

ההתאמה בין שלושת החלקים לארבעת היתרונות

כפי שצוין בפתיחה, “שלושת החלקים” הם הרכיבים, ו”ארבעת היתרונות” הם התוצאה שלהם. כשמסדרים אילו חלקים תומכים באילו יתרונות, חלוקת התפקידים שנראתה חופפת מתבהרת.

יתרון החלק שמשפיע בעיקר
1. Binary compatibility תכנון ממוקד-interfaces + IUnknown (סדר הקריאה נקבע ברמת הבינארי)
2. הפרדת interfaces תכנון ממוקד-interfaces (חושף רק את החוזה)
3. Version coexistence GUID + QueryInterface (אפשר לשאול ב-runtime על חוזה אחר)
4. Reuse מעבר לגבול process תכנון ממוקד-interfaces (אפילו המיקום של ה-implementation מנותק מהחוזה)

COM עדיין פעיל היום

נוטים לחשוב על COM כ”טכנולוגיה ישנה”, אבל הוא מנגנון שממשיך לשבת בליבת Windows גם היום.

איפה COM בשימוש

  • הרחבות Explorer (תפריט קליק ימני, תצוגה מקדימה)
  • אוטומציה של Office (שליטה חיצונית ב-Excel וב-Word)
  • Interop עם .NET (COM Interop)
  • מערכות קיימות שכוללות ActiveX
  • DirectX, Windows Shell API ועוד Windows APIs רבים

גם אם נדמה ש”זה לא קשור אליי”, כל עוד מפתחים ל-Windows, COM צץ איפשהו.

סיכום

מרכז התכנון של COM הוא “עצמאות משפה, מ-process ומ-implementation”. תכנון interfaces ניטרלי-שפה, זיהוי ייחודי וניהול גרסאות עם GUID, reference counting עם IUnknown, ומנגנון שמטפל ב-IPC בצורה שקופה.

מרכז התכנון של COMתכנון ניטרלי-שפה של interfaces, זיהוי ייחודי עם GUID, reference counting של IUnknown, ו-IPC שקוף — ארבעת המנגנונים האלה תומכים במרכז התכנון של COM: עצמאות משפה, מ-process ומ-implementation.תכנון ניטרלי-שפהעצמאות משפה, process ו-implementationזיהוי ייחודי עם GUIDreference counting של IUnknownIPC שקוף

איור 7: ארבעת המנגנונים יחד תומכים במרכז התכנון של COM — עצמאות משפה, process ו-implementation.

“יפה” היא הגדרה סובייקטיבית, אבל הנימוקים להערכה הזו קונקרטיים. הנה הבעיות ש-COM ניסה לפתור, לצד הפתרונות שלו:

מגבלה מאז (וגם היום) הפתרון של COM
ל-C++ אין ABI סטנדרטי, ולכן compiler שונה בלבד מונע reuse. גם ה-name mangling וגם פריסת האובייקטים אינם אחידים הפכו רק את “סדר טבלת ה-virtual functions” להסכם. בזכות הצמצום לנקודה הזו, השפה וה-compiler הפכו ללא רלוונטיים
כשמחליפים ספרייה, צריך לבנות מחדש את כל מה שמשתמש בה כל עוד החוזה (ה-interface) לא משתנה, אין צורך ב-rebuild. אפשר להחליף במצב בינארי בלבד
רוצים להוסיף פונקציונליות, אבל אי אפשר לשבור את ה-caller הקיים מוסיפים interface בלי לשנות את הקיים, ושואלים ב-runtime עם QueryInterface האם הוא קיים
שמות מתנגשים (אותו שם מחלקה שייך לדברים שונים שקיימים זה לצד זה) זיהוי ייחודי עם GUID. כך תיאום השמות מיותר מלכתחילה
קריאה לפונקציונליות ב-process אחר או במחשב אחר משנה לגמרי את אופן הכתיבה של הקריאה Proxy/Stub עומדים באמצע ושומרים על צורת הקוד של ה-caller זהה

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

ודרך הפתרון הזו נשארה כמעט ללא שינוי בפיתוח מונחה-components המודרני.

COM המקבילה המודרנית
קובעים את החוזה מראש ב-IDL, ומייצרים ממנו קוד לשני הצדדים מייצרים client/server מסכימת OpenAPI או Protocol Buffers
לא משנים interface קיים, אלא מוסיפים interface חדש ב-Protocol Buffers מוסיפים מספרי שדה חדשים בלי לעשות בהם reuse, מחלקים API לגרסאות
בלי תלות בשפת ה-implementation (binary contract) בלי תלות בשפת ה-implementation (חוזה הודעות דרך הרשת)
Proxy/Stub מסתירים את גבול ה-process ה-stub של RPC client מסתיר את גבול הרשת

ההבדל היחיד הוא סוג הגבול (גבול process באותו מחשב, או רשת) ואופן ייצוג החוזה (בינארי, או טקסט/סכימה). מי שמבין את COM, כשהוא ניגש לתכנון interfaces בין microservices, מבין מעצמו — לא כמגמה חולפת אלא כהכרח — למה קובעים את החוזה מראש ולמה אסור למחוק שדות קיימים.

מאמרים עדכניים עם אותן תגיות, להעמקה בנושאים קרובים.

העמודים האלה ממקמים את הנושא בהקשר רחב יותר של שירותים והחלטות.

המאמר קשור ישירות לשירותים הבאים.

שאלות נפוצות

שאלות נפוצות בפניות בנושא המאמר.

מה זה COM?
COM (Component Object Model) הוא binary contract לתקשורת בין components ב-Windows. השפות וה-compilers יכולים להיות שונים; התקשורת עוברת דרך interface שהוא חוזה קשיח. בבסיס עומדת גישת התכנון "תכנתם מול החוזה, לא מול ה-implementation".
מה זה IUnknown?
IUnknown הוא ה-interface הבסיסי שכל COM interface יורש. הוא נותן שלוש פונקציות: QueryInterface שואל אם יש interface אחר, AddRef מעלה את ה-reference count, ו-Release מוריד אותו ומשמיד את האובייקט כשהוא מגיע לאפס. ניהול חיי האובייקטים ב-COM בנוי על ה-reference count הזה.
האם COM עדיין בשימוש היום?
כן. COM ממשיך לשבת בליבת Windows. הוא מופיע בהרחבות Explorer (תפריט קליק ימני ותצוגה מקדימה), אוטומציה של Office ב-Excel וב-Word, COM Interop מול .NET, מערכות קיימות שכוללות ActiveX, DirectX ו-Windows Shell API, ועוד. כל עוד מפתחים ל-Windows, COM צץ איפשהו.
מה היתרונות של COM?
ארבעה עיקריים. binary compatibility: רכיב שנבנה פעם אחת אפשר לעשות בו reuse בלי תלות בשפה או ב-runtime. הפרדת interfaces: ה-implementation מוסתר ורק החוזה חשוף. version coexistence: מוסיפים interfaces חדשים ושומרים על backward compatibility. וקריאה בטוחה לפונקציונליות ב-process נפרד דרך Out-of-proc COM (EXE server).

פרופיל הכותב

עמוד היכרות עם כותב המאמר.

Go Komura

מנהל KomuraSoft LLC

מתמחה בפיתוח תוכנה עבור Windows, ייעוץ טכני וחקירת תקלות, בעיקר בפרויקטים עם מערכות קיימות ובאגים שקשה לשחזר.

קישורים ציבוריים

חזרה לבלוג