How I Learned to Stop Worrying and Love the COM Object

02 Sep 2026

Demystifying COM: From Machine Code to Process Isolation

If you worked through the late 1990s and early 2000s, the mention of COM (Component Object Model) probably triggers a mild sense of dread. Anyone who spent time registering DLLs using regsvr32, wrestling with undocumented ActiveX components, or debugging cryptic 0x80040154 error codes likely carries a few architectural scars.

For technical leaders today, legacy COM components are often viewed as a toxic liability, a black box of unmanaged code that stands in the way of modern infrastructure, cloud environments, and containerisation. The immediate instinct is usually a complete rewrite.

However, before throwing away decades of proven, core business logic, it helps to demystify what COM actually is. When you strip away the painful tooling and the Windows friction of the era, COM at its absolute core was simply an elegant binary standard for calling function pointers across language boundaries.


1. The Low-Level Fundamentals: COM Stripped Back

Forget Windows, the Registry, and desktop history for a moment. At its fundamental level, COM is an Application Binary Interface (ABI) built entirely on memory pointers.

[ Caller Program ]
       │
       ▼
┌───────────────┐
│ Object Pointer│
└───────┬───────┘
        │
        ▼
┌───────────────┐        [ Virtual Method Table (vtable) ]
│  vptr (RAM)   │───────►┌─────────────────────────────────┐
└───────────────┘        │ Memory Address: QueryInterface()│
                         ├─────────────────────────────────┤
                         │ Memory Address: AddRef()        │
                         ├─────────────────────────────────┤
                         │ Memory Address: Release()       │
                         ├─────────────────────────────────┤
                         │ Memory Address: BusinessMethod()│
                         └─────────────────────────────────┘

In compiled languages like C++, calling a virtual function relies on a Virtual Method Table (vtable), an array of function pointers stored in RAM. Microsoft designed COM’s binary specification to match this exact memory structure:

  1. Memory Layout: A COM object is a block of allocated RAM whose first entry (vptr) points to a table of function pointers (the vtable).
  2. Calling Rules: Functions use the standard __stdcall calling convention, and the first parameter passed to the memory address is always a pointer to the object itself (the this pointer).

Because COM standardises this binary memory layout, the calling program does not need to know C++. As long as a language, whether C, C#, Rust, Python, or Go can read a memory address, jump down an array of function pointers, and execute the address found there, it can run a COM object natively.

The Immutable Foundation: IUnknown

Every COM interface inherits from a baseline 3-method interface called IUnknown:

interface IUnknown {
    virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
    virtual ULONG   AddRef()  = 0;
    virtual ULONG   Release() = 0;
};

These three functions solve two core problems without language-specific runtimes:

  • QueryInterface (Type Safety): Asks the binary at runtime, “Do you support this specific interface contract?” If yes, it hands back a pointer to that vtable. This allows components to evolve over time without breaking existing code.
  • AddRef / Release (Memory Management): Simple reference counting. When a caller grabs a pointer, it increments the count. When finished, it decrements it. When the count hits zero, the object frees its own memory.

2. The Windows Layer: Why the Registry Was Leveraged

If COM is just memory layouts and function pointers, why did it become so painful to manage?

Enter discovery and the Windows System Layer.

       [ Client App ]
             │
             │ CoCreateInstance(CLSID_OrderProcessor)
             ▼
┌───────────────────────────┐
│ COM Runtime (ole32.dll)   │
└────────────┬──────────────┘
             │ Reads
             ▼
┌───────────────────────────┐
│ Windows Registry          │
│ HKEY_CLASSES_ROOT\CLSID\  │ ──► Maps GUID to "C:\Services\OrderProc.dll"
└───────────────────────────┘

The Windows Registry was not originally created for COM. Windows 3.1 introduced it in 1992 as a simple hierarchical database primarily to replace sprawling .INI files and handle file associations (such as double-clicking a file in File Manager to open the correct application).

When COM evolved, Microsoft needed a system-wide directory where binaries could register their capabilities without requiring application paths to be hardcoded. The Registry was already present and running, making it a convenient place to store these system-wide lookup tables.

Instead of referencing C:\Program Files\App\Service.dll, developers referenced a 128-bit Globally Unique Identifier (GUID) known as a CLSID (Class ID).

When a program calls CoCreateInstance(CLSID_OrderProcessor, ...):

  1. The Windows COM runtime looks up the CLSID in the Registry.
  2. It locates the path to the associated binary (.dll or .exe).
  3. It loads the binary into memory using LoadLibrary().
  4. It calls the binary’s internal factory function (DllGetClassObject).
  5. It hands back an IUnknown pointer to the caller.

3. Bypassing the Registry: Taming Legacy COM Today

The biggest myth about legacy COM components is that they require administrative registry installation, global OS registration, and complex Windows Server environments.

The Registry was simply an abstraction leveraged on top of COM to make component discovery easier for desktop applications. It is not COM itself.

In modern cloud environments, containers, or restricted environments like Azure App Services, modifying HKEY_CLASSES_ROOT or running elevated setup scripts is either impossible or bad practice.

Because an in-process COM object is just a standard Windows DLL exporting regular functions, you can bypass the Windows COM runtime (CoCreateInstance) entirely using standard system calls:

// 1. Manually load the DLL binary into process memory
HMODULE hDll = LoadLibrary(L"LegacyBusinessLogic.dll");

// 2. Fetch the function pointer for the DLL's internal class factory
typedef HRESULT (__stdcall *DllGetClassObjectFunc)(REFCLSID, REFIID, LPVOID*);
DllGetClassObjectFunc DllGetClassObject = 
    (DllGetClassObjectFunc)GetProcAddress(hDll, "DllGetClassObject");

// 3. Request the Class Factory directly without touching the Registry
IClassFactory* pFactory = nullptr;
DllGetClassObject(CLSID_MyObject, IID_IClassFactory, (void**)&pFactory);

// 4. Instantiate the COM Object and get its interface pointer
IMyBusinessInterface* pLogic = nullptr;
pFactory->CreateInstance(null, IID_IMyBusinessInterface, (void**)&pLogic);

// 5. Execute directly via the vtable!
pLogic->ExecuteTransaction();

Alternatively, Registration-Free COM (RegFree COM) uses side-by-side XML manifests to let Windows resolve CLSID requests directly to local application folders without touching the global registry.

However to use the side-by-side XML manifest you need to put the manifest file, well, side by side to your executable, i.e. in the same directory. Which is fine if you can do that, but isn’t if you don’t actually have access to the exe, for instance if you are on azure running as an app service or azure function. Your executable now is the w3wp.exe if you are in a windows plan, in a folder you don’t have access to.

The Real Problem with Legacy Components

Bypassing the registry solves the deployment problem, but it leaves behind the deeper, runtime realities of legacy COM DLLs:

  • Resource Leaks: Old unmanaged components frequently leak memory, handles, or GDI objects over time.
  • Thread Affinity and Instability: Many legacy components are single-threaded (STA) or rely on state that causes random process crashes under modern multi-threaded web loads.
  • Architecture Mismatches: You might have a 32-bit legacy COM DLL that cannot be loaded natively into a modern 64-bit .NET host process.

A Pragmatic Way Forward

Understanding that COM is fundamentally just function pointer invocation away from global system state opens up pragmatic options. You do not always have to choose between a complete, risky system rewrite and running a fragile, registry-bound virtual machine.

This exact challenge led to the creation of ProcessSandbox.

Rather than trying to force unstable or 32-bit COM binaries directly into a primary application process, ProcessSandbox offloads execution into isolated worker processes. It handles process lifecycle management, transparent proxying, and resource monitoring, allowing legacy COM components, or plain unmanaged DLLs, to execute safely out-of-process without bringing down the main application host.

COM brought plenty of headaches in its heyday, but stripped down to its bare metal fundamentals, it remains a predictable binary interface. Recognising where the binary specification ends and where the OS registry machinery begins gives you far more choices for managing legacy code effectively.


Appendix: A History and Ecosystem of COM

To understand why COM evolved the way it did, it helps to look at how Microsoft applied it across its enterprise products over three decades.

  • 1991–1993: 1991–1993: Dynamic Data Exchange to OLE and COM Windows needed a way for applications to embed content inside other applications (such as placing an Excel spreadsheet inside a Word document). DDE (Dynamic Data Exchange) was too fragile, leading to Object Linking and Embedding (OLE 1.0). In 1993, Microsoft released OLE 2.0, rebuilt entirely on a new underlying engine: the Component Object Model (COM).

  • 1996: 1996: ActiveX and DCOM With the rise of the commercial internet, Microsoft rebranded lightweight COM controls aimed at web browsers as ActiveX. At the same time, DCOM (Distributed COM) was released, extending the binary vtable protocol across network boundaries using DCE/RPC function calls.

  • 1997–1999: 1997–1999: MTS, COM+, and IIS Microsoft Transaction Server (MTS) introduced component-based middleware features like database connection pooling, object recycling, and automatic two-phase commit transactions. In Windows 2000, MTS was baked directly into the OS as COM+. Microsoft Internet Information Services (IIS versions 4 and 5) relied heavily on COM+ out-of-proc packages (DLLHost.exe) to isolate ASP applications and prevent web scripts from crashing the web server process.

  • 2001–2003: 2001–2003: IIS 6.0 and the Architecture Shift IIS 6.0 marked a turning point. IIS abandoned COM+ process hosting in favour of native HTTP.sys kernel-mode listening and native worker process isolation (Application Pools). COM was moved back to being an execution implementation rather than an architectural hosting framework.

  • 2002–Present: 2002–Present: .NET, WinRT, and Cloud Scale While .NET introduced managed assemblies, COM remained the underlying binary substrate for core Windows subsystems. The Windows Runtime (WinRT) introduced in Windows 8, which powers modern Windows App SDKs and UWP applications, is built directly on top of enhanced COM interfaces (inheriting from IInspectable, which inherits from IUnknown). COM remains widely used today inside Office automation engines, Windows system services, and underlying graphics APIs like DirectX.