Caso práctico de puente COM: cómo llamar a una DLL de 64 bits desde una app de 32 bits

· Actualizado el: · · COM, Desarrollo en Windows, 32bit, 64bit

El requisito de llamar a una DLL de 64 bits desde una aplicación de 32 bits es bastante habitual en Windows. Sobre todo cuando se quiere conservar los activos existentes y usar únicamente las funciones del lado de 64 bits, la configuración de un puente COM suele ser la solución más realista.

Público al que va dirigido: personas que mantienen una aplicación de Windows de 32 bits existente y quieren usar una DLL o biblioteca del lado de 64 bits. Está escrito para poder leerse partiendo de un nivel de «he oído hablar de COM, pero nunca lo he programado yo mismo».

Entorno previo: Windows de 64 bits (x64) y un entorno de desarrollo donde pueda escribir en C# (Visual Studio, por ejemplo). Registrar un servidor COM para toda la máquina (bajo HKEY_LOCAL_MACHINE) requiere permisos de administrador. Las ideas básicas de COM están explicadas en «Qué es COM: por qué el diseño de COM en Windows sigue siendo hermoso».

Índice

  1. Situación planteada
  2. Solución
  3. Flujo de procesamiento (diagrama de secuencia)
  4. Código de ejemplo (idea general)
  5. Código de ejemplo completo
  6. Resumen
  7. Referencias

1. Situación planteada

Se trata de casos en los que se quiere usar el procesamiento de una DLL de 64 bits dejando intacta la aplicación de 32 bits existente. Sin embargo, un proceso de 32 bits no puede cargar una DLL de 64 bits. Se trata de una restricción a nivel de sistema operativo, no de algo que se pueda resolver con algún truco.

Las situaciones más frecuentes son estas:

  • La aplicación de 32 bits existente es un activo grande y no se puede migrar de inmediato
  • El lado de la DLL de 64 bits tiene funciones nuevas, o las bibliotecas de las que depende solo existen en 64 bits
  • Se quiere llamar desde el lado de 32 bits «con tipado»

Con esta combinación, la vía de llamar dentro del mismo proceso está cerrada desde el principio.

2. Solución

A partir de este apartado aparecen varios términos técnicos, así que antes vamos a repasar el vocabulario mínimo necesario.

Término Significado
In-proc COM (servidor DLL) Forma de usar un componente COM cargándolo en el mismo proceso que quien lo invoca. Es rápido, pero no se puede cargar si los bits no coinciden
Out-of-proc COM (servidor EXE) Forma de usar un componente COM iniciándolo como un proceso independiente. Permite la comunicación aunque los bits sean distintos
LocalServer Un servidor COM que se ejecuta como otro proceso dentro del mismo equipo. La ruta del EXE se registra en la clave de registro LocalServer32
IDL / TypeLib La definición (IDL) que describe la forma de la interfaz (nombres de métodos, tipos de argumentos) y su versión binaria (TypeLib). Se usan para que ambos lados vean el mismo «contrato»
Marshaling (serialización) Reempaquetar los argumentos y valores de retorno en una forma que se pueda enviar al cruzar los límites de proceso. Lo inverso es el unmarshaling
Proxy / Stub Código intermediario que realiza el marshaling propiamente dicho. El Proxy se ubica en el lado que llama y el Stub en el lado del servidor
WOW6432Node El lugar donde se almacena físicamente el contenido del registro destinado a aplicaciones de 32 bits en un Windows de 64 bits. Aunque el nombre de la clave sea el mismo, el contenido difiere entre el lado de 32 y el de 64 bits

La base de la solución es separar los procesos mediante Out-of-proc COM (servidor EXE). La DLL de 64 bits se llama desde un servidor COM de 64 bits (EXE), y la aplicación de 32 bits la utiliza a través de COM.

El flujo es el siguiente.

  1. Preparar un COM LocalServer de 64 bits (EXE) que internamente llame a la DLL de 64 bits
  2. Compartir la interfaz COM (IDL/TypeLib) para exponer los tipos
  3. La aplicación de 32 bits llama a COM «con tipado» (comunicándose mediante Proxy/Marshal)

Sin embargo, también hay puntos a tener en cuenta.

  • El registro de 32 bits y de 64 bits es independiente (incluido WOW6432Node)
  • Las estructuras propias requieren un diseño de marshaling
  • Existe sobrecarga de IPC, así que hay que tener cuidado con las llamadas de alta frecuencia

En resumen, la vía más segura es «sacar el procesamiento de 64 bits a otro proceso y tender un puente con COM».

3. Flujo de procesamiento (diagrama de secuencia)

A continuación se muestra el flujo cuando una aplicación de 32 bits llama al procesamiento de una DLL de 64 bits.

Diagrama de secuencia del puente COM de 32 y 64 bitsMuestra cómo la aplicación cliente de 32 bits llama a la DLL de 64 bits a través de un servidor COM local, con el marshaling y el Proxy o Stub cruzando el límite de procesoInfraestructura de marshaling COM ya registradaDLL de 64 bitsServidor COM de 64 bits(EXE)COM Stub(lado de 64 bits)RPC/IPC(comunicación entre procesos)COM Proxy(lado de 32 bits)Aplicación cliente de 32 bitsDLL de 64 bitsServidor COM de 64 bits(EXE)COM Stub(lado de 64 bits)RPC/IPC(comunicación entre procesos)COM Proxy(lado de 32 bits)Aplicación cliente de 32 bitsSerializar los parámetros (marshaling)Deserializar los parámetros (unmarshaling)Serializar el valor de retorno (marshaling)Deserializar el valor de retorno (unmarshaling)ICalcService.Add(1, 2)Datos serializadosTransferencia a través del límite de procesoAdd(1, 2)Llamada a función nativaResultado: 3Resultado: 3Resultado serializadoTransferencia a través del límite de procesoResultado: 3

Puntos clave:

  • La aplicación de 32 bits puede llamar de forma segura en cuanto a tipos a través de la interfaz ICalcService
  • El runtime de COM cruza el límite de proceso usando la DLL de Proxy/Stub registrada, el marshaler de TypeLib, el marshaler estándar, etc.
  • Como existe sobrecarga en la comunicación entre procesos, es preferible el procesamiento por lotes frente a las llamadas finas

4. Código de ejemplo (idea general)

4.1. Interfaz compartida, servidor y cliente

Lo siguiente es una idea conceptual. Para que funcione, hace falta además el registro descrito en 4.2.

// Interfaz compartida (equivalente a IDL)
[ComVisible(true)]
[Guid("7A4B5B23-0A2F-4D2B-9D4D-8A2A92B8B001")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface ICalcService
{
    int Add(int a, int b);
}

// COM LocalServer de 64 bits (lado EXE)
[ComVisible(true)]
[Guid("1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("KomuraSoft.CalcService")]
public class CalcService : ICalcService
{
    public int Add(int a, int b)
    {
        // Aquí se llama a la DLL de 64 bits
        return a + b;
    }
}

// Lado de la aplicación de 32 bits (cliente)
Type t = Type.GetTypeFromProgID("KomuraSoft.CalcService");
var calc = (ICalcService)Activator.CreateInstance(t);
int result = calc.Add(1, 2);

Con esta forma, el lado de 32 bits puede tratarlo «con tipado». COM utiliza internamente el proxy/stub y realiza la llamada a través de IPC.

El atributo [ProgId("KomuraSoft.CalcService")] está para que el cliente pueda encontrarlo con Type.GetTypeFromProgID("KomuraSoft.CalcService"). El ProgID no es más que un «alias legible para humanos»; lo que realmente localiza el servidor es el registro del CLSID, que se explica a continuación.

4.2. Pasos mínimos de registro

COM funciona bajo el principio de que «el runtime de COM busca el CLSID registrado en el registro de Windows y lo inicia», así que el código que no está registrado nunca funcionará (Type.GetTypeFromProgID devuelve null, o CreateInstance produce REGDB_E_CLASSNOTREG). En el fondo, el registro necesario para un servidor EXE (LocalServer) se reduce a estas tres claves.

Qué se registra Clave Valor
Correspondencia ProgID → CLSID HKEY_CLASSES_ROOT\KomuraSoft.CalcService\CLSID {1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
CLSID → ruta del EXE HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\LocalServer32 Ruta completa del EXE del servidor COM de 64 bits
CLSID → ProgID (búsqueda inversa) HKEY_CLASSES_ROOT\CLSID\{1C9B6F4D-…}\ProgID KomuraSoft.CalcService

Aquí aparece la trampa que constituye el propio tema de este artículo. La documentación de Microsoft indica explícitamente que HKEY_LOCAL_MACHINE\SOFTWARE\Classes se comparte entre las aplicaciones de 32 y 64 bits, mientras que la subclave CLSID que cuelga de ahí (junto con Interface, entre otras) es distinta para el lado de 32 bits y el de 64 bits (el contenido real del lado de 32 bits está en WOW6432Node). Es decir, la clave del ProgID, una vez escrita, es visible desde ambos lados, pero el registro del CLSID debe escribirse tanto en la vista de 32 bits como en la de 64 bits, o el cliente de 32 bits no encontrará el servidor.

Lo más seguro es usar las opciones /reg:32 y /reg:64 del comando reg desde un símbolo del sistema con permisos de administrador (Microsoft desaconseja escribir Wow6432Node manualmente en la ruta).

:: Ejecutar desde un símbolo del sistema con permisos de administrador
set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService
set SERVER=C:\Program Files\KomuraSoft\CalcServer.exe

:: 1) ProgID → CLSID (justo bajo HKLM\SOFTWARE\Classes se comparte entre 32/64)
reg add "HKLM\SOFTWARE\Classes\%PROGID%\CLSID" /ve /d "%CLSID%" /f

:: 2) CLSID → LocalServer32 y ProgID (bajo CLSID es distinto para 32/64, así que hay que escribir en ambos)
::    En el valor de LocalServer32 se guarda la ruta del ejecutable junto con las comillas.
::    Si se escribe /d "%SERVER%", el análisis de argumentos de reg.exe elimina las comillas y el valor
::    queda almacenado tal cual, C:\Program Files\... . COM interpreta esto como una línea de comandos,
::    así que primero intenta buscar C:\Program.exe, cortado antes del espacio
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID"        /ve /d "%PROGID%"     /f /reg:64
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /d "\"%SERVER%\"" /f /reg:32
reg add "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\ProgID"        /ve /d "%PROGID%"     /f /reg:32

:: Comprobar el valor almacenado. Es correcto si aparece entre comillas como "C:\Program Files\..."
reg query "HKLM\SOFTWARE\Classes\CLSID\%CLSID%\LocalServer32" /ve /reg:64

Las comillas en LocalServer32 no son solo una cuestión de buenas formas. Sin comillas, C:\Program Files\... admite la interpretación de que se está pasando Files\... como argumento a C:\Program. Como resultado, en un entorno donde alguien tenga permisos para crear C:\Program.exe, ese ejecutable podría iniciarse primero. Si la ruta contiene espacios, incluya siempre las comillas.

Para darlo de baja basta con eliminar las mismas claves.

set CLSID={1C9B6F4D-1E9A-4E61-9A4F-6A0F1D2D9A11}
set PROGID=KomuraSoft.CalcService

reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:64
reg delete "HKLM\SOFTWARE\Classes\CLSID\%CLSID%" /f /reg:32
reg delete "HKLM\SOFTWARE\Classes\%PROGID%" /f

Además, al iniciarse, el lado EXE debe anunciarle a COM que «él es el responsable de este CLSID». En C/C++ esto corresponde a CoRegisterClassObject, y en .NET Framework a RegistrationServices.RegisterTypeForComClients. El registro en el registro de Windows solo se encarga de que COM llegue a iniciar el EXE, así que si falta este anuncio se produce un fallo confuso: el EXE se inicia, pero no se puede crear el objeto.

Por otro lado, si solo se trata de probar durante el desarrollo, se puede registrar sin permisos de administrador escribiendo la misma estructura en HKCU\SOFTWARE\Classes en lugar de HKLM (HKEY_CLASSES_ROOT es una vista combinada de HKLM y HKCU). No obstante, HKCU\SOFTWARE\Classes\CLSID también se trata de forma distinta para 32 y 64 bits, así que sigue siendo necesario escribir en ambas vistas.

4.3. La forma de construirlo difiere entre .NET Framework y .NET (5 y posteriores)

El código anterior está en C#, pero el procedimiento cambia mucho según qué versión de .NET se use. Mezclar ambas es una fuente segura de bloqueos.

  .NET Framework .NET (Core 3.0 / 5 y posteriores)
Herramienta de registro Existe RegAsm.exe (pero genera un registro InprocServer32 para in-proc, así que LocalServer32 hay que escribirlo igualmente a mano) No existe una herramienta equivalente a RegAsm
Exposición estándar a COM Añadir atributos al ensamblado y usar RegAsm Generar *.comhost.dll con <EnableComHosting>true</EnableComHosting> y registrarlo con regsvr32 (solo in-proc)
Generación de TypeLib (.tlb) Se puede generar con TlbExp / RegAsm /tlb No compatible. Hay que escribir el IDL a mano y compilarlo con MIDL (desde .NET 6 es posible incrustar el .tlb resultante en el comhost)
Especificación del CLSID Opcional Las clases que COM debe generar requieren indicar el CLSID explícitamente
Tratamiento de AnyCPU Se puede usar desde clientes de 32 y 64 bits El *.comhost.dll asociado es de 64 bits de forma predeterminada, así que solo se puede usar desde clientes de 64 bits

La configuración de este artículo (servidor EXE) queda fuera del alcance del EnableComHosting estándar en .NET (5 y posteriores), por lo que el proceso de registro hay que escribirlo por cuenta propia. Microsoft ofrece la muestra oficial OutOfProcCOM, así que si se va a construir con .NET, ese es el punto de partida.

5. Código de ejemplo completo

En GitHub se publica un ejemplo que implementa los conceptos anteriores de forma que realmente funciona.

Call64bitDLLFrom32bitProc - GitHub

Este repositorio incluye lo siguiente:

  • Call64bitDLLFrom32bitProc/ - COM LocalServer de 64 bits (EXE)
  • X64DLL/ - DLL de 64 bits (procesamiento real)
  • X86App/ - Cliente de 32 bits (WinForms)
  • scripts/ - Scripts de registro y baja del servidor COM

Si compila y registra siguiendo los pasos descritos en el README, podrá comprobar en la práctica cómo un proceso de 32 bits llama a una DLL de 64 bits.

6. Resumen

El puente COM no es una solución universal: es una configuración en la que queda muy claro qué tareas encajan y cuáles no. Antes de decidir adoptarlo, contraste su caso con la siguiente tabla.

Casos en los que encaja Casos en los que no encaja
No se puede reconstruir la aplicación de 32 bits en sí (el coste de la modificación no compensa) Se puede recompilar directamente el lado de 32 bits a 64 bits (esa es la vía más corta)
Las llamadas tienen granularidad gruesa (una imagen por llamada, un archivo por llamada, etc.) Se realizan llamadas finas con alta frecuencia, como decenas de miles de veces elemento por elemento (la sobrecarga de IPC pasa a dominar)
Lo que se intercambia son tipos fáciles de serializar, como números, cadenas o arreglos Se hacen ir y volver en masa punteros crudos o estructuras propias complejas
Se quiere mantener viva la aplicación principal aunque falle el procesamiento del lado de 64 bits (la separación de procesos es una ventaja) No se quiere escribir lógica de recuperación para manejar caídas o reinicios del lado del servidor
Se quiere conservar llamadas con tipado (IntelliSense o comprobación en tiempo de compilación) Basta con un procesamiento por lotes puntual, y es suficiente con intercambiar datos por entrada/salida estándar o archivos

Como reverso de la última fila, siempre vale la pena considerar la alternativa sencilla de «convertir el procesamiento del lado de 64 bits en un simple EXE de consola e intercambiar datos mediante argumentos y archivos». El puente COM resulta útil cuando se quiere mantener llamadas con tipado y cuando se quiere invocar repetidamente un servidor con estado.

Como próximos pasos, se recomienda seguir este orden.

  1. Primero, clone el repositorio de ejemplo del capítulo 5, compile y registre según el README, y cree localmente una instancia que funcione.
  2. Elija una sola función de su DLL de 64 bits y añada un método correspondiente a la interfaz equivalente a ICalcService del ejemplo, haciéndolo pasar.
  3. Una vez que funcione, mida el número de llamadas y la cantidad de datos por llamada. Resolver de antemano la decisión de diseño de «orientarse hacia una granularidad gruesa» (agrupar varias llamadas en una sola) reduce los retrocesos posteriores.

7. Referencias

  • Introducción al Component Object Model (COM) https://learn.microsoft.com/en-us/windows/win32/com/component-object-model–com–portal
  • Registro de COM LocalServer32 https://learn.microsoft.com/en-us/windows/win32/com/localserver32
  • Fundamentos de las interfaces COM https://learn.microsoft.com/en-us/windows/win32/com/the-component-object-model
  • COM Interop (uso desde .NET) https://learn.microsoft.com/en-us/dotnet/standard/native-interop/cominterop
  • Redirector de registro de WOW64 (HKLM\SOFTWARE\Classes se comparte, la subclave CLSID es distinta para 32/64) https://learn.microsoft.com/en-us/windows/win32/winprog64/shared-registry-keys
  • Exponer componentes de .NET (Core / 5 y posteriores) a COM https://learn.microsoft.com/en-us/dotnet/core/native-interop/expose-components-to-com

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.

¿Se puede llamar directamente a una DLL de 64 bits desde una aplicación de 32 bits?
No es posible. Un proceso de 32 bits no puede cargar una DLL de 64 bits, y esta es una restricción a nivel de sistema operativo, no algo que se pueda evitar con algún truco. Como la vía de llamar dentro del mismo proceso está cerrada desde el principio, es necesario separar el procesamiento del lado de 64 bits en otro proceso.
¿Cómo se puede usar una función de una DLL de 64 bits desde una aplicación de 32 bits?
La vía más segura es separarlos mediante Out-of-proc COM (servidor EXE). La DLL de 64 bits se llama desde un COM LocalServer (EXE) de 64 bits, y la aplicación de 32 bits la utiliza con tipado a través de una interfaz COM. Se comparte la interfaz COM (IDL/TypeLib) para exponer los tipos, y el runtime de COM cruza el límite de proceso mediante el proxy/stub y la comunicación entre procesos.
¿Cuáles son los puntos a tener en cuenta en una configuración de puente COM?
Hay principalmente tres. Que el registro de 32 bits y de 64 bits es independiente (incluido WOW6432Node), que las estructuras propias requieren un diseño de marshaling, y que existe sobrecarga en la comunicación entre procesos, por lo que hay que tener cuidado con las llamadas finas y de alta frecuencia. Es preferible orientarse al procesamiento por lotes antes que emitir un gran volumen de llamadas finas.
¿Existe un código de ejemplo que funcione realmente?
Sí. En el repositorio de GitHub Call64bitDLLFrom32bitProc se publica un conjunto de ejemplo completo que incluye un COM LocalServer (EXE) de 64 bits, una DLL de 64 bits, un cliente de 32 bits (WinForms) y los scripts de registro y baja del servidor COM. Si se compila y registra siguiendo los pasos del README, se puede comprobar en la práctica cómo un proceso de 32 bits llama a una DLL de 64 bits.

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