Qué es COM — por qué el diseño de Windows COM sigue siendo hermoso hoy
· Actualizado el: · Go Komura · 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.
flowchart LR
accTitle: El contrato binario de COM entre el lado que llama y la implementación
accDescr: Diagrama 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.
subgraph CALLER["Lado que llama ── el lenguaje no importa"]
A1["Aplicación en C++"]
A2["Aplicación en C#"]
A3["VBA, Python, etc."]
end
CONTRACT["Contrato (la parte fija en binario)<br/>• El IID (GUID) determina de forma única «qué contrato es»<br/>• El orden comienza con los 3 métodos de IUnknown<br/>• Tipos de argumentos y valores de retorno, y convención de llamada de cada método"]
subgraph IMPL["Implementación ── el lenguaje no importa"]
B1["Componente escrito en C++"]
B2["Componente escrito en C#"]
end
A1 --> CONTRACT
A2 --> CONTRACT
A3 --> CONTRACT
CONTRACT --> B1
CONTRACT --> B2
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
AddRefuno mismo. TantoCoCreateInstancecomoQueryInterfaceya han aplicadoAddRefal puntero que devuelven. Es decir, el principio es que «quien recibe el puntero es quien llama aRelease», y solo se llama explícitamente aAddRefcuando se empieza a retener el mismo puntero en otro lugar adicional. - Que
QueryInterfacefalle 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».
flowchart LR
accTitle: Coexistencia de versiones mediante QueryInterface entre componentes antiguos y nuevos
accDescr: Diagrama 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.
OLDC["Lado que llama antiguo<br/>solo conoce ICalcService"]
NEWC["Lado que llama nuevo<br/>intenta preguntar por ICalcServiceEx"]
OLDS["Componente de versión antigua<br/>solo implementa ICalcService"]
NEWS["Componente de versión nueva<br/>implementa ambas"]
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK"| OLDS
OLDC -->|"QueryInterface(IID_ICalcService)<br/>S_OK ── no se rompe aunque se actualice"| NEWS
NEWC -->|"QueryInterface(IID_ICalcServiceEx)<br/>S_OK ── se puede usar la función nueva"| NEWS
NEWC -.->|"QueryInterface(IID_ICalcServiceEx)<br/>E_NOINTERFACE ── continúa con la función antigua"| OLDS
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 relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Trampas de registro y bitness en el desarrollo COM/OCX/ActiveX
Un repaso práctico de los problemas más comunes en COM, OCX y ActiveX: bitness de 32/64 bits, Visual Studio 2022, regsvr32/Regasm, permis...
Qué son COM, ActiveX y OCX - Diferencias y relación explicadas
Explicamos qué son COM, ActiveX y OCX: diferencias, relación, vínculo con OLE, dónde se usan y cómo abordarlos hoy desde una perspectiva ...
Cómo funcionan el portapapeles y arrastrar y soltar — Gestionar correctamente la transferencia de datos OLE en aplicaciones empresariales
Una tabla de Excel se deforma al pegarla y deja de poder pegarse si cierra el origen: es el portapapeles colocando el mismo contenido en ...
Externalización y desarrollo por encargo de una app Windows: qué aclarar antes de solicitarlo
Antes de encargar la externalización o el desarrollo por encargo de una app Windows, estos son los puntos a aclarar: revisión de software...
El extraño amor de un desarrollador, o: cómo aprendí a dejar de preocuparme y amar Windows
Windows es incómodo. Pero esa incomodidad es también la de un sistema operativo que lleva décadas cargando sobre sus espaldas el trabajo ...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Migración de ActiveX
Decisiones para conservar, encapsular o sustituir componentes COM / ActiveX / OCX.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Reutilización y migración de activos existentes
Entender el diseño y la compatibilidad de COM es un buen punto de partida para pensar cómo aprovechar los activos existentes de Windows.
Consultoría técnica y revisión de diseño
Si desea definir un rumbo teniendo en cuenta IUnknown, GUID y el diseño de límites, esto conecta con nuestra consultoría técnica y revisión de diseño.
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.