Qué es COM — por qué el diseño de Windows COM sigue siendo hermoso hoy

· Actualizado el: · · COM, ActiveX, Desarrollo en Windows

¿Qué es COM?

COM (Component Object Model) es el «contrato binario» que permite a los componentes comunicarse entre sí en Windows. Es un mecanismo de comunicación basado en un contrato estricto: la interfaz, que trasciende las diferencias de lenguaje o compilador, y en cuya base subyace la filosofía de diseño de «programar contra el contrato, no contra la implementación».

Los tres elementos clave de COM

Lo que se presenta en este capítulo son las piezas que componen el mecanismo llamado COM. El apartado siguiente, «Las cuatro ventajas de COM», describe las propiedades que resultan de combinar estas piezas, así que no se está explicando dos veces lo mismo. La correspondencia entre ambos se muestra en una tabla al final del capítulo de ventajas.

1. Diseño centrado en la interfaz

En COM, «el contrato va antes que la implementación». Sin conocer la implementación interna de un objeto, basta con conocer la interfaz publicada para poder utilizarlo.

2. Identificación mediante GUID (CLSID / IID)

A todo componente y a toda interfaz se les asigna un identificador único a nivel mundial (GUID), de modo que la colisión de nombres simplemente no puede ocurrir.

3. IUnknown

Es la interfaz básica de la que heredan todas las interfaces COM. Ofrece las tres funciones siguientes.

Método Función
QueryInterface Pregunta si el objeto tiene otra interfaz
AddRef Incrementa el contador de referencias
Release Decrementa el contador de referencias (se destruye a sí mismo al llegar a cero)

Cuando se combinan estos tres elementos, lo único que queda entre el lado que llama y la implementación es «una interfaz identificada por un GUID, con un orden de métodos fijo». Ni el lenguaje ni el compilador aparecen en este contrato.

El contrato binario de COM entre el lado que llama y la implementaciónDiagrama que muestra cómo distintos lados que llaman, en C++, C# o VBA y Python, se conectan a un contrato binario fijo formado por el IID, el orden de los métodos de IUnknown y los tipos de argumentos y valores de retorno de cada método, contrato que a su vez se implementa en componentes escritos en C++ o C#, sin que el lenguaje aparezca en ningún punto del contrato.Implementación ── el lenguaje no importaLado que llama ── el lenguaje no importaComponente escrito en C++Componente escrito en C#Aplicación en C++Aplicación en C#VBA, Python, etc.Contrato (la parte fija en binario)• El IID (GUID) determina de forma única «qué contrato es»• El orden comienza con los 3 métodos de IUnknown• Tipos de argumentos y valores de retorno, y convención de llamada de cada método

Figura 1: El contrato binario de COM. Ni el lenguaje del lado que llama ni el de la implementación aparecen en el contrato, así que se puede sustituir cualquiera de los dos sin necesidad de recompilar al otro.

El código que muestra «programar contra el contrato»

Solo con texto resulta difícil de entender, así que veámoslo en el código mínimo posible. Es un ejemplo del componente ICalcService, que se limita a sumar, utilizado sin conocer en absoluto su implementación.

Empecemos por C++ (COM sin envoltorios). La definición de ICalcService que aparece aquí es el contrato en sí mismo, y en ningún punto del código del lado que llama aparece si la implementación está escrita en C++ o en C#.

#include <objbase.h>

// Definición del contrato. Normalmente este tipo de encabezado se genera a partir de un IDL
// Al heredar de IUnknown, siempre tiene 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;
};

// Contrato de la versión extendida, añadida más tarde. El GUID es distinto
struct __declspec(uuid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B002"))
ICalcServiceEx : public ICalcService
{
    virtual HRESULT STDMETHODCALLTYPE Multiply(int a, int b, int* result) = 0;
};

// CLSID del componente de implementación. Normalmente se define en el encabezado generado a partir del IDL
static const CLSID CLSID_CalcService =
    { 0x1C9B6F4D, 0x1E9A, 0x4E61, { 0x9A, 0x4F, 0x6A, 0x0F, 0x1D, 0x2D, 0x9A, 0x11 } };

// Lado que llama
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));
// El puntero devuelto aquí ya tiene AddRef aplicado (el contador de referencias es 1)

if (SUCCEEDED(hr))
{
    int sum = 0;
    hr = calc->Add(1, 2, &sum);          // Se puede llamar sin conocer la implementación

    // Pregunta en tiempo de ejecución «¿también tiene el contrato de la versión extendida?» = implementación de la coexistencia de versiones
    ICalcServiceEx* calcEx = nullptr;
    if (SUCCEEDED(calc->QueryInterface(__uuidof(ICalcServiceEx),
                                       reinterpret_cast<void**>(&calcEx))))
    {
        // Solo se llega aquí si es la nueva versión del componente
        calcEx->Release();               // Quien lo recibió lo libera
    }
    // Si no lo tiene, simplemente devuelve E_NOINTERFACE y la versión antigua sigue funcionando

    calc->Release();                     // El contador de referencias llega a 0 y se destruye
}
CoUninitialize();

Hay tres puntos que merece la pena destacar.

  • Casi nunca hay que escribir AddRef uno mismo. Tanto CoCreateInstance como QueryInterface ya han aplicado AddRef al puntero que devuelven. Es decir, el principio es que «quien recibe el puntero es quien llama a Release», y solo se llama explícitamente a AddRef cuando se empieza a retener el mismo puntero en otro lugar adicional.
  • Que QueryInterface falle no es una anomalía. «No tengo ese contrato» (E_NOINTERFACE) es una respuesta normal, y es justamente lo que hace posible «añadir funciones nuevas sin romper los componentes antiguos».
  • Todos los valores de retorno son HRESULT. Devolver el éxito o el fracaso mediante un valor de retorno, en lugar de una excepción, es una convención necesaria para cruzar lenguajes. La forma de lanzar excepciones varía según el lenguaje, pero un valor de retorno entero lo puede interpretar cualquiera.

Usar el mismo contrato desde C# se ve así. Si el GUID es el mismo, se considera el mismo contrato, así que no importa que la otra parte esté implementada en C++.

using System;
using System.Runtime.InteropServices;

// Se escribe el mismo GUID que en el lado de C++. Esta es la declaración de «mismo contrato»
[ComImport]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
    int Add(int a, int b);
}

// Lado que llama
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService")
         ?? throw new InvalidOperationException("El servidor COM no está registrado.");
object server = Activator.CreateInstance(t)!;
try
{
    var calc = (ICalcService)server;   // Este cast equivale a QueryInterface
    int sum = calc.Add(1, 2);
    Console.WriteLine(sum);            // 3
}
finally
{
    Marshal.ReleaseComObject(server);  // Equivale a Release
}

En el lado de C#, que el valor de retorno de Add sea int se debe a que la interoperabilidad COM de .NET aplica una conversión fija: «convertir el último argumento [out, retval] en el valor de retorno, y transformar los HRESULT de fallo en excepciones». AddRef/Release también dejan de ser visibles para quien llama, pero no han desaparecido: un envoltorio ligero que provee .NET (el RCW) los llama en su lugar. Usar el mismo contrato con la forma natural de escribir de cada lenguaje: esta es la forma real que adopta el «contrato binario».

Las cuatro ventajas de COM

1. Compatibilidad binaria

Un componente ya compilado se puede reutilizar sin importar el lenguaje de programación o el entorno de ejecución. Por eso es habitual poder llamar desde C# o Python a un componente COM creado en C++.

2. Separación de interfaces

Como oculta por completo la implementación y solo expone el contrato, se puede cambiar libremente la implementación interna sin que ello afecte al lado que llama.

3. Coexistencia de versiones

Para añadir funciones manteniendo la compatibilidad hacia atrás, el diseño básico consiste en ir añadiendo nuevas interfaces. Así se pueden ofrecer funciones nuevas sin modificar las interfaces antiguas.

Al combinar los lados que llaman antiguos y nuevos con los componentes antiguos y nuevos, tres de las cuatro combinaciones funcionan tal cual, y en la combinación restante basta con que la respuesta sea «no lo tengo».

Coexistencia de versiones mediante QueryInterface entre componentes antiguos y nuevosDiagrama que muestra cómo un lado que llama antiguo y uno nuevo consultan mediante QueryInterface a un componente de versión antigua y a uno de versión nueva, obteniendo S_OK en tres de las cuatro combinaciones y E_NOINTERFACE solo cuando el lado nuevo pregunta al componente antiguo por la interfaz extendida.QueryInterface(IID_ICalcService)S_OKQueryInterface(IID_ICalcService)S_OK ── no se rompe aunque se actualiceQueryInterface(IID_ICalcServiceEx)S_OK ── se puede usar la función nuevaQueryInterface(IID_ICalcServiceEx)E_NOINTERFACE ── continúa con la función antiguaLado que llama antiguosolo conoce ICalcServiceLado que llama nuevointenta preguntar por ICalcServiceExComponente de versión antiguasolo implementa ICalcServiceComponente de versión nuevaimplementa ambas

Figura 2: Al añadir un IID nuevo sin modificar la interfaz existente, cualquier combinación de versiones sigue funcionando. La línea de puntos representa la respuesta normal de «no tengo ese contrato».

4. Reutilización más allá de los límites de proceso

Un componente COM se puede colocar de dos formas. In-proc (servidor DLL), que se carga como DLL en el mismo proceso que el lado que llama, y Out-of-proc (servidor EXE, LocalServer), que se inicia como EXE en un proceso distinto. El aspecto del código que lo invoca no cambia en ninguno de los dos casos.

  In-proc (servidor DLL) Out-of-proc (servidor EXE)
Dónde se ejecuta En el mismo proceso que el lado que llama En un proceso distinto
Cómo se realiza la llamada Llamada directa mediante puntero a función Se reempaquetan los argumentos (marshaling) y se transmiten por comunicación entre procesos
Velocidad Rápida Más lenta, por la comunicación entre procesos
Si el otro lado se cae El lado que llama también cae El lado que llama sobrevive (la llamada simplemente falla)
Arquitectura (32/64 bit) Debe coincidir para poder cargarse Puede ser distinta

Con COM Out-of-proc (servidor EXE) se pueden invocar de forma segura funciones de otro proceso. La última fila, «la arquitectura de 32/64 bit puede ser distinta», resulta útil en la práctica, y el planteamiento concreto para usar una DLL de 64 bits desde una aplicación de 32 bits se aborda en «Puente COM real para llamar a una DLL de 64 bits desde una aplicación de 32 bits».

Ahora bien, que sea un proceso distinto también significa que el otro lado puede desaparecer en cualquier momento. Cuando el proceso servidor se bloquea o termina, al lado que llama le llegan los siguientes fallos. No representan que «algo esté roto», sino que «el otro ya no está», y son errores propios de Out-of-proc.

Código de error Significado
RPC_E_DISCONNECTED El objeto al que se intentaba llamar está desconectado del cliente (el objeto remoto ya no existe)
RPC_S_SERVER_UNAVAILABLE No se puede llegar al servidor (proceso) al que se dirige la llamada

Lo que en In-proc no había que pensar («si el otro se cae, yo también me caigo») entra en el diseño de Out-of-proc como procesamiento de recuperación de reinicio o reconexión. Conviene entenderlo como trabajo adicional a cambio de mayor seguridad.

Correspondencia entre los tres elementos y las cuatro ventajas

Como se mencionó al principio, los «tres elementos» son las piezas y las «cuatro ventajas» son su resultado. Al alinear qué pieza sostiene qué ventaja, queda claro el reparto de papeles que antes parecía solaparse.

Ventaja Elemento que la sostiene principalmente
1. Compatibilidad binaria Diseño centrado en la interfaz + IUnknown (el orden de llamada queda fijado a nivel binario)
2. Separación de interfaces Diseño centrado en la interfaz (solo se expone el contrato)
3. Coexistencia de versiones GUID + QueryInterface (se puede preguntar por otro contrato en tiempo de ejecución)
4. Reutilización más allá de los límites de proceso Diseño centrado en la interfaz (el contrato desliga incluso el lugar de la implementación)

COM sigue vigente hoy

Suele considerarse una «tecnología antigua», pero COM es un mecanismo que sigue utilizándose en el núcleo de Windows.

Lugares donde se usa COM

  • Extensiones del Explorador (menús contextuales, vistas previas)
  • Automatización de Office (control externo de Excel y Word)
  • Interoperabilidad con .NET (COM Interop)
  • Sistemas existentes que incluyen ActiveX
  • DirectX, Windows Shell API y muchas otras API de Windows

Aunque se piense que «no tiene nada que ver conmigo», mientras se desarrolle para Windows, COM aparecerá en algún punto.

Resumen

El núcleo del diseño de COM es «la independencia respecto al lenguaje, al proceso y a la implementación». Un diseño de interfaces neutral respecto al lenguaje, una identificación única y una gestión de versiones mediante GUID, un recuento de referencias a través de IUnknown, y un mecanismo que trata de forma transparente la comunicación entre procesos.

Decir que es «hermoso» es una valoración subjetiva, pero los motivos para afirmarlo son concretos. Si se ponen uno junto a otro los problemas que COM intentó resolver y sus respuestas, quedan así.

Restricción de la época (y que sigue vigente) Respuesta de COM
C++ no tiene una convención binaria estándar (ABI), así que basta con cambiar de compilador para perder la reutilización; ni el decorado de nombres ni la disposición de los objetos coinciden Convirtió en convención únicamente «el orden de la tabla de funciones virtuales». Al limitarse a este único punto, dejó de importar el lenguaje o el compilador
Al sustituir una biblioteca, hay que recompilar todo lo que la usa Mientras el contrato (la interfaz) no cambie, no hace falta recompilar: se puede sustituir en binario
Se quiere añadir una función, pero no se puede romper el lado que ya llama Se añade una interfaz sin modificar las existentes, y se pregunta en tiempo de ejecución si «la tiene» mediante QueryInterface
Los nombres colisionan (conviven distintas cosas con el mismo nombre de clase) Se identifica de forma única mediante GUID. Elimina de raíz la necesidad de coordinar nombres
Al llamar a una función de otro proceso o de otra máquina, la forma de escribir la llamada cambia por completo Se interpone un Proxy/Stub para mantener el código del lado que llama con la misma forma

En otras palabras, el diseño de COM no partió de «cómo hacerlo elegante», sino que llegó a su forma actual como resultado de eliminar, uno por uno, los obstáculos concretos que impedían la reutilización. No son muchos los diseños en los que se puede seguir con tanta claridad la correspondencia entre restricción y solución.

Y esta forma de resolver los problemas permanece intacta en el desarrollo orientado a componentes actual.

COM Equivalente actual
El contrato se decide primero en el IDL, y a partir de ahí se genera el código de ambos lados Generar cliente/servidor a partir de esquemas de OpenAPI o Protocol Buffers
Se añaden interfaces nuevas sin modificar las existentes En Protocol Buffers no se reutilizan los números de campo, se añaden nuevos; las API se dividen en versiones
Independiente del lenguaje de implementación (contrato binario) Independiente del lenguaje de implementación (contrato de mensajes a través de la red)
El Proxy/Stub oculta el límite de proceso El stub cliente de RPC oculta el límite de red

Lo único que cambia es el tipo de límite (el de proceso dentro de la misma máquina, o la red) y la forma de expresar el contrato (binaria, o textual/esquema). Entender COM hace que, al diseñar interfaces entre microservicios, preguntas como «por qué se decide el contrato primero» o «por qué no se deben eliminar campos existentes» dejen de sentirse como una moda y se entiendan como una necesidad.

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Qué es COM?
COM (Component Object Model) es un «contrato binario» para que los componentes se comuniquen entre sí en Windows. Es un mecanismo que permite la comunicación mediante un contrato estricto —la interfaz— por encima de las diferencias de lenguaje o compilador. En la base subyace la filosofía de diseño de «programar contra el contrato, no contra la implementación».
¿Qué es IUnknown?
Es la interfaz básica de la que heredan todas las interfaces COM. Ofrece tres funciones: QueryInterface, que pregunta si el objeto tiene otra interfaz; AddRef, que incrementa el contador de referencias; y Release, que lo decrementa y destruye el objeto cuando llega a cero. La gestión del ciclo de vida de los objetos COM se sostiene sobre este contador de referencias.
¿Se sigue usando COM hoy en día?
Sí, COM es un mecanismo que sigue utilizándose en el núcleo de Windows. Aparece en muchos lugares: las extensiones del Explorador (menús contextuales y vistas previas), la automatización de Office en Excel y Word, la interoperabilidad COM con .NET (COM Interop), sistemas existentes que incluyen ActiveX, y numerosas API como DirectX o Windows Shell API. Mientras se desarrolle para Windows, COM aparecerá en algún punto.
¿Cuáles son las ventajas de COM?
Son principalmente cuatro. La compatibilidad binaria, que permite reutilizar un componente ya compilado sin importar el lenguaje o el entorno de ejecución; la separación de interfaces, que oculta la implementación y solo expone el contrato; la coexistencia de versiones, que mantiene la compatibilidad hacia atrás mediante la adición de nuevas interfaces; y la invocación segura de funciones de otro proceso mediante COM fuera de proceso (Out-of-proc COM, servidor EXE).

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog