El problema de EXCEL.EXE que permanece al operar Excel desde C# — Patrones de liberación de referencias COM y el criterio de sustitución
· Actualizado el: · Go Komura · Excel, C#, COM, .NET, .NET Framework, Office, Mantenimiento de legado, Consultoría técnica
«Implementé una función que genera informes con Excel, y el Administrador de tareas terminó lleno de filas de EXCEL.EXE.» «Cerré la aplicación, pero al intentar abrir el archivo la siguiente vez me dice que otro proceso lo está usando.» «En el servidor del proceso por lotes nocturno se acumularon varios cientos de procesos de Excel, agotaron la memoria y el sistema se detuvo.» Prácticamente todos los desarrolladores que han escrito código en C# para manipular Excel mediante Microsoft.Office.Interop.Excel se han topado alguna vez con este fenómeno. A nuestra empresa también nos llegan consultas periódicas del tipo «llamo a Quit() pero Excel no termina».
Lo complicado es que este problema parece ocurrir «solo de vez en cuando». Desaparece en la máquina de desarrollo pero permanece en producción; permanece en ejecución de depuración pero desaparece en la versión release: como las condiciones de reproducción son inestables, el remedio sintomático Process.Kill tiende a colarse en el código de producción. Pero la causa no es cuestión de suerte: se explica por completo mediante el conteo de referencias de COM y el mecanismo del RCW (Runtime Callable Wrapper) de .NET. Este artículo organiza, desde una perspectiva práctica, el mecanismo por el que el proceso permanece, la trampa clásica de la «regla de los dos puntos», la comparación de las dos corrientes de patrones de liberación junto con nuestra recomendación, la forma correcta de aplicar el Kill de proceso como último recurso, y el criterio para decidir la sustitución por bibliotecas de la familia Open XML.
1. Primero, la conclusión
En una frase: Quit() no es una liberación, así que EXCEL.EXE no termina hasta que se devuelvan todas las referencias COM que retiene el RCW. A continuación se detalla el desglose y las medidas.
- Que EXCEL.EXE permanezca no es un error, sino que el lado de .NET todavía retiene la referencia COM.
Quit()es solo una solicitud de «terminar cuando se liberen todas las referencias»; si queda alguna referencia, Excel sigue esperando fielmente. - .NET maneja los objetos COM a través de un envoltorio llamado RCW (Runtime Callable Wrapper), y mantiene la referencia al objeto COM hasta que el propio RCW es recolectado por el GC (o liberado explícitamente).1
- Como en
book.Worksheets[1].Range["A1"], al encadenar dos o más puntos, el RCW del objeto intermedio se genera sin quedar asignado a ninguna variable, y se convierte en una fuga de liberación. Se conoce popularmente como la «regla de los dos puntos», y es el principal culpable de este problema. - Existen dos corrientes de patrones de liberación: (a) aplicar
Marshal.ReleaseComObjectcon disciplina a todos los objetos COM, y (b) confinar las referencias en variables y dejar que las recolecte el GC (GC.Collect+WaitForPendingFinalizers). La documentación oficial sitúa a ReleaseComObject como algo que «solo debe usarse cuando es absolutamente necesario».2 - Nuestra recomendación es tomar como base el patrón de GC con el procesamiento aislado en un único método, e introducir un pequeño envoltorio que disciplina la liberación con using solo cuando es necesario controlar el orden de liberación (capítulo 4).
- Como seguro para los casos que aun así permanecen, se guarda el identificador de ventana a partir de
Application.Hwnd, se identifica el PID conGetWindowThreadProcessIdy se hace Kill. No se utiliza el método de identificarlo por la diferencia entre la lista de procesos antes y después del inicio, porque puede terminar arrastrando accidentalmente a un Excel que el usuario tiene abierto.34 - Para empezar, la automatización de Office en el lado del servidor o en entornos desatendidos no cuenta con soporte de Microsoft.5 Si se trata de la generación desatendida de informes, considere primero sustituirla por Open XML SDK / ClosedXML, que no inician Excel (capítulos 6 y 7).67
2. Por qué permanece EXCEL.EXE — el conteo de referencias y el RCW
Primero, el principio del lado COM. La vida de un objeto COM se gestiona mediante conteo de referencias, y el servidor de automatización de Excel (EXCEL.EXE) no termina hasta que se devuelven todas las referencias entregadas a clientes externos. Ya en la época de VB6, olvidar llamar a Release dejaba procesos residuales de la misma manera (para repasar COM, véase «Qué es COM / ActiveX / OCX»).
A continuación, el lado de .NET. El código C# no toca el objeto COM directamente, sino que opera a través de un proxy llamado RCW que genera el CLR. Se crea un RCW por cada objeto COM dentro del proceso, este cachea el puntero de la interfaz COM y libera la referencia al objeto COM cuando el propio RCW es recolectado por el GC.1 Es decir, la gestión del ciclo de vida pasa del esquema de «contar uno mismo el conteo de referencias» al esquema de «dejarlo en manos del GC».
Al combinar estos dos hechos se deduce directamente la razón por la que permanece EXCEL.EXE. Si se representa en un diagrama la cadena de referencias, queda de la siguiente manera.
flowchart LR
accTitle: Cadena de referencias entre variables .NET, RCW y el conteo de referencias COM de EXCEL.EXE
accDescr: Muestra cómo las variables y los objetos intermedios anónimos alimentan el RCW, cómo el RCW retiene el conteo de referencias COM dentro de EXCEL.EXE, y que solo Marshal.ReleaseComObject o la recolección por el GC reducen ese conteo, mientras que Quit() únicamente lo notifica sin liberar nada
subgraph NET["Dentro del proceso .NET"]
V["Variables<br/>excel / book / sheet"]
A["Objeto intermedio anónimo<br/>valor de retorno de book.Worksheets, etc.<br/>Sin variable, no se puede liberar manualmente (capítulo 3)"]
R["RCW<br/>Uno por cada objeto COM<br/>Devuelve la referencia COM al ser recolectado por el GC"]
end
subgraph EXE["Dentro de EXCEL.EXE"]
CNT["Conteo de referencias COM<br/>No disminuye hasta que se devuelven todas las referencias entregadas"]
PROC["EXCEL.EXE no termina"]
end
REL["Marshal.ReleaseComObject<br/>o recolección por el GC (capítulo 4)"]
QUIT["excel.Quit()<br/>Solo indica «puedes terminar»<br/>no reduce ninguna referencia"]
V --> R
A --> R
R -->|"Retiene la referencia COM"| CNT
CNT --> PROC
REL -.->|"Aquí es donde actúa"| R
QUIT -.->|"Actúa aquí pero no libera"| PROC
Ya venga de la «variable» del extremo izquierdo o del «objeto intermedio anónimo», mientras el RCW retenga la referencia COM el extremo derecho no terminará. De las dos flechas inferiores del diagrama, la única que actúa es la que va de REL a R. Siguiendo las etapas, queda así.
| Etapa | Qué ocurre |
|---|---|
new Excel.Application() |
Se inicia EXCEL.EXE y se crea el RCW de Application |
| Operaciones con celdas, guardado, etc. | Por cada objeto tocado (Workbook, Worksheet, Range…) aumenta el número de RCW |
excel.Quit() |
Solo le indica a Excel «puedes terminar». Ninguna de las referencias que retiene el RCW disminuye |
| Se sale del método | Desaparece la referencia .NET al RCW, pero el propio RCW sigue vivo en el heap |
| GC (en algún momento) | El RCW es recolectado y solo entonces se devuelve la referencia COM → EXCEL.EXE termina |
Hay dos puntos clave. Primero, Quit() no es una liberación: mientras queden referencias, Excel espera. Lo normal es que, cuando el proceso de la aplicación termina por completo, las referencias se cortan y Excel también termina, pero en formas como una aplicación residente o una aplicación web, donde el proceso padre sigue vivo, ese «algún momento» nunca llega. Segundo, el momento de la liberación es indeterminado porque depende del GC. Si hay margen de memoria, el GC puede no ejecutarse durante decenas de minutos, y mientras tanto EXCEL.EXE permanece como un proceso zombi. La falta de reproducibilidad de «a veces permanece» o «solo permanece en producción» no es más que la variabilidad del momento del GC, tal cual.
Cabe señalar que un Excel iniciado con Visible = false no tiene ventana, así que aunque permanezca no es visible para el usuario. Lo típico es que el síntoma aparezca como «error de archivo en uso en el segundo guardado» o «el equipo va lento», y que la fila de EXCEL.EXE se descubra recién al abrir el Administrador de tareas. El primer paso de la investigación es contar el número con tasklist | findstr EXCEL.
3. La trampa clásica: la «regla de los dos puntos» — los objetos intermedios invisibles
En el código de las consultas del tipo «libero correctamente todas las variables, pero el proceso permanece», casi siempre aparece una línea como esta.
// A primera vista parece limpio, pero genera RCW que no se pueden liberar
excel.Workbooks.Open(path);
book.Worksheets[1].Range["A1"].Value2 = "hello";
excel.Workbooks genera y devuelve el RCW de la colección Workbooks. Si se llama a .Open(...) sin recibir este valor de retorno en una variable, el RCW de Workbooks queda en el heap como un objeto anónimo «vivo pero al que nadie hace referencia». Como no hay variable, tampoco hay forma de llamar a Marshal.ReleaseComObject. La segunda línea es aún más grave: en una sola línea genera tres RCW anónimos: Worksheets (la colección), Worksheets[1] (la hoja) y Range["A1"] (el rango).
La regla empírica para evitar esto es la que la comunidad de automatización de Office lleva años llamando la «regla de los dos puntos». Se puede reformular como: no encadenar dos o más puntos sobre un objeto COM; recibir siempre cada objeto intermedio en una variable.
// Se le da nombre a cada objeto intermedio
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(path);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";
Puede parecer redundante, pero esta forma de escribir sirve para «dejar en un estado en el que se puede enumerar todo lo que hay que liberar». A continuación se enumeran también variantes derivadas que se pasan por alto con facilidad.
- foreach:
foreach (Excel.Worksheet s in book.Worksheets)genera RCW para la colección, el enumerador y cada uno de los elementos. En la corriente de ReleaseComObject, lo habitual es usar un bucle for con índice y recibir cada elemento en una variable. - Descarte dentro de una expresión condicional: también se generan RCW dentro de expresiones como
if (excel.Workbooks.Count > 0). - Argumentos de una expresión compuesta: una expresión como
sheets.Add(After: sheets[sheets.Count])crea varios RCW anónimos en una sola línea. - Suscripción a eventos: al asociar un controlador a un evento de Application o Workbook, esa conexión retiene una referencia. Es imprescindible cancelar la suscripción antes de terminar.
Por último, hablemos de la fuente. El propio nombre «regla de los dos puntos» proviene de la comunidad y no es terminología oficial de Microsoft. Sin embargo, el contenido de la regla tiene respaldo en fuentes primarias. El documento de soporte de Microsoft que trata el problema de que las aplicaciones de Office no terminan después de la automatización menciona, como primera condición para lograr que terminen, «declarar cada objeto como una variable nueva», y muestra que, en lugar de encadenar como en oExcel.Workbooks.Add(), conviene interponer una vez oBooks = oExcel.Workbooks.8 Si se pone esto junto a que «se crea un RCW por cada objeto COM, y libera la referencia cuando es recolectado por el GC»1, se puede rastrear solo con fuentes primarias por qué se filtran los objetos intermedios. El mismo documento también menciona, como remedio para cuando la liberación no basta para que termine, llamar a GC.Collect y GC.WaitForPendingFinalizers.8 Considere que las dos corrientes del siguiente capítulo son, ambas, una extensión de este documento.
4. Comparación de los patrones de liberación — la corriente de ReleaseComObject y la corriente de GC
Hay dos corrientes para escribir código que garantice que EXCEL.EXE termine. Ambas funcionan si se escriben correctamente. El problema es si se puede «seguir escribiendo correctamente», y ahí es donde aparece la diferencia en la práctica.
4.1 (a) La corriente que aplica Marshal.ReleaseComObject con disciplina
Marshal.ReleaseComObject decrementa el conteo interno de referencias del RCW y, en el momento en que llega a 0, libera de inmediato la referencia COM que retiene el RCW.2 La ventaja es que se puede liberar en un momento determinista, sin esperar al GC.
Como el código se vuelve largo, veamos primero solo el esqueleto. Lo que hay que hacer es «recibir en una variable todo lo que se usa y liberarlo en orden inverso al de creación», pero Close y Quit también son llamadas COM y por lo tanto pueden fallar; por eso el punto clave es anidar try/finally para garantizar que, aunque salte una excepción a mitad de camino, siempre se llegue a las liberaciones posteriores.
try
Application → Workbooks → Workbook → Sheets → Worksheet → Range ← orden de creación
finally
Liberar Range, Worksheet, Sheets ← orden inverso al de creación
try book.Close()
finally
Liberar Workbook, Workbooks
try excel.Quit()
finally
Liberar Application
Al escribir esto directamente en C# queda de la siguiente manera. Se ve largo porque incluye, sin omitir nada, este anidamiento y todas las comprobaciones de null.
using Excel = Microsoft.Office.Interop.Excel;
using System.Runtime.InteropServices;
Excel.Application excel = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range cell = null;
try
{
// No se usa un inicializador de objeto (new ... { DisplayAlerts = false }).
// Si la llamada COM del setter falla, excel quedaría sin asignar al entrar en finally,
// y no se podría ni hacer Quit ni liberar el EXCEL.EXE ya iniciado
excel = new Excel.Application();
excel.DisplayAlerts = false;
books = excel.Workbooks;
book = books.Open(templatePath);
sheets = book.Worksheets;
sheet = (Excel.Worksheet)sheets[1];
cell = sheet.Range["A1"];
cell.Value2 = "hello";
book.SaveAs(outputPath);
}
finally
{
// Se libera en orden inverso al de creación. Como Close y Quit también son llamadas COM
// que pueden fallar, se anida try/finally para garantizar que, aunque salte una excepción
// a mitad de camino, siempre se llegue a Quit y a la liberación
if (cell != null) Marshal.ReleaseComObject(cell);
if (sheet != null) Marshal.ReleaseComObject(sheet);
if (sheets != null) Marshal.ReleaseComObject(sheets);
try
{
if (book != null) book.Close(SaveChanges: false);
}
finally
{
if (book != null) Marshal.ReleaseComObject(book);
if (books != null) Marshal.ReleaseComObject(books);
try
{
if (excel != null) excel.Quit();
}
finally
{
if (excel != null) Marshal.ReleaseComObject(excel);
}
}
}
El punto débil de este método, como se ve, es que el costo de la disciplina es alto. Hay que recibir en una variable, sin excepción, cada objeto COM que se toca, y liberarlo en orden inverso incluyendo la ruta de excepciones. Es más, si se considera que el propio Close o Quit puede fallar (error COM, libro ya desconectado, Excel que no responde), hay que garantizar, como arriba, con try/finally anidados, que «aunque falle a mitad de camino, se llegue a las liberaciones posteriores». Por experiencia, exigir que todos los miembros del equipo mantengan esto en cada modificación es bastante difícil. Basta con que se cuele una sola expresión compuesta de dos puntos en un solo lugar para que la fuga reaparezca.
Más importante aún, la propia documentación oficial advierte contra el abuso. La referencia de Marshal.ReleaseComObject deja claro que es un recurso para cuando hay que liberar recursos de forma oportuna o cuando el orden de liberación importa, y que «use ReleaseComObject only if it is absolutely required» (solo debe usarse cuando es absolutamente necesario).2 Como el RCW es un mecanismo que comparte una única instancia por objeto COM dentro del proceso, si un RCW liberado en un lugar del código todavía se usa en otro lugar, se produce InvalidComObjectException o, en el peor de los casos, una violación de acceso o corrupción de memoria del proceso.2 En una estructura donde varios módulos de la aplicación comparten la operación de Excel, este accidente ocurre en la práctica.
Cabe señalar que, si el mismo puntero de interfaz se pasa varias veces al CLR, el conteo de referencias del RCW puede superar 1, y en ese caso no se libera con una sola llamada. Existe también Marshal.FinalReleaseComObject, que fuerza el conteo a 02, pero el momento en que se necesita esta API es en sí una señal de que no se está controlando la gestión del ciclo de vida, así que en nuestra empresa recomendamos revisar el diseño.
Una advertencia de seguridad más, en un eje distinto al de la permanencia del proceso. Un libro abierto con Workbooks.Open mediante automatización COM puede ejecutar VBA sin advertencia de macros. Los ejemplos de este artículo parten de la premisa de abrir una plantilla de confianza gestionada por la propia aplicación, pero si existe la posibilidad de abrir un libro de origen externo —un archivo de una carpeta compartida, un archivo subido por un usuario, etc.— configure excel.AutomationSecurity = MsoAutomationSecurity.msoAutomationSecurityForceDisable (espacio de nombres Microsoft.Office.Core) antes de Open para deshabilitar las macros de forma forzada.9 Esto cierra el vector de ataque en el que sustituir la plantilla se convierte directamente en ejecución de código arbitrario. Esta advertencia también se aplica tal cual al ejemplo del patrón de GC descrito más adelante.
4.2 (b) La corriente que confina las referencias y deja que las recolecte el GC
La otra corriente consiste en dejar la liberación del RCW en manos del GC, tal como funciona por diseño, y ejecutar ese GC en un momento determinado. Como el RCW libera la referencia COM al ser recolectado por el GC1, al «ejecutar un GC completo y esperar a que terminen los finalizadores después de que todas las referencias que tocan Excel salgan de su ámbito», se puede lograr que EXCEL.EXE termine sin escribir ni una sola vez ReleaseComObject.
using System.Runtime.CompilerServices;
using Excel = Microsoft.Office.Interop.Excel;
public void ExportReport(string templatePath, string outputPath)
{
try
{
// El procesamiento que toca Excel se aísla por completo en otro método
ExportReportCore(templatePath, outputPath);
}
finally
{
// Precisamente la ruta de excepciones es donde EXCEL.EXE tiende a permanecer,
// así que se ejecuta siempre en finally.
// Una vez que se sale del método, no queda ninguna referencia al RCW en ningún sitio
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect(); // segunda vuelta que recolecta los RCW que los finalizadores desligaron
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath)
{
var excel = new Excel.Application();
try
{
excel.DisplayAlerts = false;
Excel.Workbooks books = excel.Workbooks;
Excel.Workbook book = books.Open(templatePath);
Excel.Sheets sheets = book.Worksheets;
Excel.Worksheet sheet = (Excel.Worksheet)sheets[1];
Excel.Range cell = sheet.Range["A1"];
cell.Value2 = "hello";
book.SaveAs(outputPath);
book.Close(SaveChanges: false);
}
finally
{
excel.Quit();
}
}
Hay tres condiciones para que esto funcione.
- Aislar el código que toca Excel en un único método. Mientras el JIT mantenga vivas las referencias en la pila, el GC no puede recolectarlas, así que el GC debe llamarse siempre «fuera del método que tocó Excel».
NoInliningsirve para evitar que el aislamiento se invalide por la expansión inline. - No dejar escapar el RCW a un campo o a un valor de retorno. Si aunque sea uno se filtra fuera del método, Excel permanecerá mientras esa referencia siga viva.
- Usar el conjunto de tres pasos
GC.Collect→GC.WaitForPendingFinalizers→GC.Collect. Como la limpieza del RCW se realiza a través del finalizador, lo habitual son dos vueltas: detectarlo con el primer Collect, esperar a que terminen los finalizadores y recolectar los restos con el segundo Collect.
Como advertencia, en una ejecución con el depurador adjunto, la vida de las variables se extiende hasta el final del método, por lo que incluso con este método el RCW puede no recolectarse. Esta es la verdadera causa del fenómeno «permanece en depuración pero desaparece en release», así que la verificación de funcionamiento debe hacerse siempre en compilación release y sin depurador.
La ventaja de este método es que no hay fugas aunque se rompa la regla de los dos puntos. El GC recolecta en bloque todo lo que perdió sus referencias, incluidos los RCW intermedios anónimos. El punto a revisar en un code review se reduce a uno solo: «¿el procesamiento que toca Excel queda encerrado en este método?», por lo que el costo de disciplina baja drásticamente. La desventaja es el mal olor de código que supone la llamada explícita a GC.Collect (la pausa provocada por un GC completo de toda la aplicación) y el riesgo de que un miembro que desconoce el motivo lo rompa con una refactorización bienintencionada. Deje siempre un comentario que explique el motivo del conjunto de tres pasos.
4.3 Nuestra recomendación — aislamiento + GC como base, y un envoltorio disciplinado solo si es necesario
Comparamos ambas posturas desde una perspectiva práctica.
| (a) Corriente ReleaseComObject | (b) Corriente GC | |
|---|---|---|
| Momento de liberación | Determinista (en el instante de la llamada) | Cuasi determinista (en el instante del conjunto de tres pasos del GC) |
| Costo de disciplina | Alto. Todos deben respetar la asignación a variable de todos los objetos y la liberación en orden inverso | Bajo. Basta con respetar el aislamiento en un método |
| Síntoma si se abusa | InvalidComObjectException, violación de acceso2 | Pausa por GC forzado |
| Alineación con la guía oficial | «Solo cuando es absolutamente necesario»2 | Se ajusta a la gestión de vida propia del RCW (dejarlo en manos del GC)1 |
| Escenario adecuado | Cuando el orden de liberación importa. Procesos de larga duración que usan Excel muchas veces de forma puntual | La mayoría del procesamiento de informes que se puede completar en un solo lugar con «abrir, escribir, cerrar» |
Nuestra recomendación es que, como base, se use el patrón (b) de aislamiento + GC. Un procesamiento como la generación de informes se cierra de forma natural en un solo método, y con solo adoptar esa forma las fugas dejan de ocurrir estructuralmente. También se evita el riesgo de abuso de ReleaseComObject, y se alinea con la posición oficial.
Use (a) solo cuando de verdad necesite orden y liberación inmediata, y en ese caso no permita escribir ReleaseComObject a pelo: introduzca un pequeño envoltorio disciplinado con using.
using System.Runtime.InteropServices;
/// <summary>Envoltorio que libera un objeto COM dentro de un ámbito using</summary>
public readonly struct ComScope<T> : IDisposable where T : class
{
public T Value { get; }
public ComScope(T value) => Value = value;
public void Dispose()
{
if (Value is not null && Marshal.IsComObject(Value))
Marshal.ReleaseComObject(Value);
}
}
using var books = new ComScope<Excel.Workbooks>(excel.Workbooks);
using var book = new ComScope<Excel.Workbook>(books.Value.Open(templatePath));
using var sheets = new ComScope<Excel.Sheets>(book.Value.Worksheets);
using var sheet = new ComScope<Excel.Worksheet>((Excel.Worksheet)sheets.Value[1]);
using var cell = new ComScope<Excel.Range>(sheet.Value.Range["A1"]);
cell.Value.Value2 = "hello";
book.Value.SaveAs(outputPath);
book.Value.Close(SaveChanges: false);
Como Dispose se ejecuta en el orden inverso al de declaración de los using, «liberar en orden inverso al de creación» queda garantizado por el propio mecanismo del lenguaje. El código crece por el .Value, pero la disciplina se comprime en una única regla: «un objeto COM siempre se recibe con ComScope». Dicho de otro modo, si se escribe una expresión compuesta como books.Value.Open(...).Worksheets, la fuga reaparece, así que de todas formas hace falta enseñar la regla de los dos puntos.
Dos advertencias comunes a ambos patrones. Primero, no dejar que aparezca el cuadro de diálogo de confirmación de guardado antes de Quit(). Especifique explícitamente DisplayAlerts = false y Close(SaveChanges: false). Si un Excel oculto se cuelga esperando un cuadro de diálogo, el propio Quit no llega a completarse. Segundo, como el COM de Excel presupone STA, no manipule la misma instancia de Application desde varios hilos. La relación entre los hilos y los apartamentos COM está organizada en «Fundamentos de COM STA/MTA».
4.4 Procedimiento para confirmar que el problema se resolvió
Una vez introducido el patrón de liberación, conviene fijar también el procedimiento para confirmar si «de verdad ya desaparece». Como las condiciones de reproducción de este problema son inestables, si no se fija un procedimiento se puede confundir «una vez que desapareció por casualidad» con un éxito.
- Ejecutar en compilación release, sin depurador adjunto. Como se explicó en el apartado 4.2, si el depurador está adjunto, la vida de las variables se extiende hasta el final del método, y ni siquiera un patrón de GC bien escrito logra que se recolecte el RCW. Desde Visual Studio, es la opción «Iniciar sin depurar».
- Contar el número de EXCEL.EXE antes de empezar. Ejecute
tasklist /FI "IMAGENAME eq EXCEL.EXE"y anote cuántos Excel ya están en ejecución (incluidos los que el usuario tiene abiertos manualmente). En PowerShell,@(Get-Process EXCEL -ErrorAction SilentlyContinue).Countda directamente el número. - Ejecutar el mismo proceso 10 veces o más de forma consecutiva. Con una sola vez, puede desaparecer por casualidad según el momento del GC.
- Esperar unos segundos después de terminar el proceso y contar de nuevo. Si vuelve al mismo número del paso 2, es un éxito. Si aumentó, el aumento es directamente el número de fugas.
- Repetir el mismo procedimiento también en la ruta de excepciones. Provoque un fallo deliberado a mitad de camino, por ejemplo con una ruta de plantilla inexistente o un destino de guardado de solo lectura, y confirme que el número de todos modos vuelve a la normalidad. La mayoría de los accidentes en los que permanece EXCEL.EXE ocurren no en la ruta normal, sino en la ruta de excepciones, así que si se omite este paso no se puede decir que se haya verificado.
- Si es una aplicación residente, contar sin cerrar la aplicación. Como al terminar el proceso las referencias se cortan y Excel también cae, contar después de cerrar la aplicación no verifica nada (capítulo 2).
Si se guardan en el registro los números de los pasos 2 y 4, más adelante se podrá notar si el código de liberación sufre una regresión. Si se ha implementado el Kill de seguro del capítulo 5, solo se puede afirmar que «la liberación funciona correctamente» después de confirmar también que el Kill no se ha activado (que no aparece el registro de advertencia que se emite al activarse).
5. Último recurso — identificar el PID a partir de Hwnd para eliminarlo con seguridad
Incluso implementando correctamente el patrón de liberación, quedan casos como «Excel no termina por culpa de un complemento» o «inevitablemente queda 1 proceso en alguna ruta anómala de excepción». En un proceso por lotes desatendido, el proceso residual puede provocar el bloqueo de archivo del trabajo del día siguiente, así que vale la pena implementar un Kill de proceso como último seguro. El problema es el método para identificar «cuál EXCEL.EXE matar».
Un error frecuente es identificarlo por la diferencia entre la lista de procesos antes y después del inicio: comparar Process.GetProcessesByName("EXCEL") antes y después del inicio y considerar la diferencia como la propia instancia. Esto es frágil frente a la concurrencia. Si el usuario abre Excel manualmente mientras se toma la diferencia, se produce una detección errónea, y si se ejecutan en paralelo trabajos con el mismo esquema, se confunden entre sí. El peor accidente de este método es matar un Excel sin guardar que el usuario está editando y perder sus datos, algo que de hecho nos han traído como consulta.
El método correcto de identificación es obtener el identificador de la ventana de nivel superior mediante la propiedad Hwnd del propio objeto Application que se inició, y obtener el ID del proceso que creó esa ventana con la API Win32 GetWindowThreadProcessId.34 Como el handle es propio y exclusivo de la instancia iniciada, no hay margen para confundirlo con otro Excel.
using System.Diagnostics;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
internal static class NativeMethods
{
[DllImport("user32.dll")]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
}
public void ExportReport(string templatePath, string outputPath)
{
// Con un parámetro out, aunque salte una excepción a mitad de la operación de Excel,
// el handle queda en el llamador, y así también en la ruta de excepciones
// (la ruta donde de verdad hace falta el seguro) se llega siempre a GC y a Kill
Process excelProcess = null;
try
{
ExportReportCore(templatePath, outputPath, out excelProcess);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
if (excelProcess != null)
{
KillIfStillAlive(excelProcess); // seguro que no hace nada si terminó con normalidad
excelProcess.Dispose();
}
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void ExportReportCore(string templatePath, string outputPath, out Process excelProcess)
{
excelProcess = null;
var excel = new Excel.Application();
// Si se inicia con éxito, aunque falle la obtención de Hwnd o la apertura del handle,
// se envuelve en try/finally justo después para garantizar que siempre se llegue a Quit()
try
{
// Como después de Quit() la ventana desaparece y no se puede obtener, se guarda
// justo después del inicio. GetProcessById solo asocia el PID; el handle de proceso
// del SO se abre de forma diferida en el primer acceso a WaitForExit / Kill, etc.
// Por eso, mientras Excel sigue vivo, se toca SafeHandle aquí para forzar la
// apertura del handle (mientras el handle está abierto, este PID no se reutiliza
// para otro proceso)
NativeMethods.GetWindowThreadProcessId((IntPtr)excel.Hwnd, out uint pid);
excelProcess = Process.GetProcessById((int)pid);
_ = excelProcess.SafeHandle;
excel.DisplayAlerts = false;
// …… Operación con Excel ……
}
finally
{
excel.Quit();
}
}
private void KillIfStillAlive(Process excelProcess)
{
if (!excelProcess.WaitForExit(5000)) // si termina con normalidad, desaparece en pocos segundos
{
logger.LogWarning("EXCEL.EXE (PID {Pid}) no termina, forzando su cierre", excelProcess.Id);
excelProcess.Kill();
}
}
Puntos a tener en cuenta en la implementación.
- Guardar
Hwndjusto después del inicio. Después deQuit()la ventana se destruye y no se puede obtener.Application.Hwndse puede obtener incluso conVisible = false.3 - El Kill es «un seguro sobre una liberación hecha correctamente». Si se confía en él desde el principio, no se ejecuta la limpieza de los archivos temporales de Excel, lo que provoca cosas como la acumulación de archivos de recuperación automática en el siguiente inicio. El orden debe ser siempre «liberación → Quit → espera → Kill solo si sigue vivo».
- Cuidado con la reutilización del PID. Como los PID de Windows se reutilizan, si el diseño consiste en recordar solo el número de PID y resolverlo más tarde, existe el riesgo de que, si Excel termina mientras tanto y el mismo PID se asigna a otro proceso, se mate un proceso sin relación alguna. Tenga en cuenta que llamar aquí a
Process.GetProcessByIdno es suficiente por sí solo. Como el handle de proceso del SO se abre de forma diferida en el primer acceso, conWaitForExit/Kill, etc., solo se evita la confusión al tocar explícitamenteSafeHandlemientras Excel sigue vivo para abrir el handle, como en el código anterior (mientras el handle está abierto, ese PID no se reutiliza). - Este seguro cubre el caso en que el método ya retornó pero EXCEL.EXE permanece. Si la propia llamada COM se cuelga (
Workbooks.Open,SaveAs,Quit, etc., esperando un cuadro de diálogo modal oculto o la respuesta de un complemento), no se llega afinallyy este Kill tampoco se activa. Conviene pensar la solución en dos capas. Primero, cerrar el origen de los cuadros de diálogo conDisplayAlerts = falsey elAutomationSecuritymencionado antes. Además, en un proceso por lotes desatendido, conviene preparar una estructura en la que el propio trabajo que toca Excel se ejecute como un proceso independiente, al que se le pueda imponer un límite de tiempo desde fuera y matarlo por completo (la configuración de «tiempo hasta detener la tarea» del Programador de tareas, o el Kill del proceso hijo desde el proceso padre). Un tiempo de espera dentro del propio proceso no puede interrumpir una llamada COM colgada, así que es más fiable colocar el límite a nivel de proceso. - Cuando el Kill se activa, regístrelo siempre en el log y vigile su frecuencia. Si aumenta, es una señal de una regresión en el código de liberación o de que conviene revisar el criterio de sustitución del capítulo 7.
6. Una cuestión de fondo — la automatización de Office en el lado del servidor no tiene soporte
Con las técnicas vistas hasta aquí, la permanencia de EXCEL.EXE en una aplicación cliente se puede controlar casi por completo. Sin embargo, para el escenario donde este problema se agrava más —la automatización de Excel en un servidor o en un entorno desatendido—, hay una postura oficial que conviene confirmar antes que la técnica.
Microsoft, en el documento «Considerations for server-side Automation of Office», afirma explícitamente que no recomienda ni da soporte a la automatización de Office desde aplicaciones o componentes cliente desatendidos y no interactivos (incluyendo ASP, ASP.NET, DCOM y servicios de NT).5 Office está diseñado bajo la premisa de que existe un usuario interactivo, y premisas que no son un problema en el escritorio —un diseño que muestra un cuadro de diálogo de confirmación en caso de error y espera una respuesta, componentes que presuponen el perfil del usuario que ejecuta, una arquitectura basada en STA no reentrante, etc.— muestran sus colmillos en un servicio o en un proceso de trabajo de IIS.10 Consultas como «al ejecutarlo desde un servicio de Windows, Open no vuelve en producción» o «en IIS se cuelga una vez al mes esperando un cuadro de diálogo» tenían todas su causa en esta configuración sin soporte. En esta configuración, la permanencia de EXCEL.EXE no es solo un problema de limpieza, sino que se convierte en un problema operativo en el que los procesos colgados se acumulan indefinidamente.
Lo que Microsoft plantea como alternativa es editar directamente el archivo en formato Open XML, sin instalar ni iniciar Office, y la Microsoft Graph API, que procesa del lado de la nube.10 Open XML SDK es una biblioteca de Microsoft que permite leer y escribir con clases fuertemente tipadas el formato de archivo de Office (.xlsx, etc.) estandarizado como ECMA-376 / ISO/IEC 29500; como está construida sobre ZIP y XML, no necesita el propio Excel.6 Los problemas de permanencia de procesos, licencias y configuración sin soporte desaparecen todos a la vez.
Sin embargo, Open XML SDK es una biblioteca que «edita directamente el formato de archivo» y no ofrece el comportamiento de la aplicación Excel. Como consideración de diseño oficial, se especifica que no ofrece comportamientos de aplicación como el recálculo de fórmulas o la actualización de datos, ni funciones de conversión a otros formatos (PDF, etc.).11 Además, como la API es fiel a la estructura del formato de archivo, incluso escribir una sola celda con el SDK puro requiere entender la estructura de SpreadsheetML. Lo que compensa esto es ClosedXML, un OSS con licencia MIT que superpone sobre la API de Open XML una API intuitiva de «libro, hoja, celda». Permite manejar .xlsx / .xlsm sin instalar Excel (el formato antiguo .xls queda fuera).7 La distinción de uso en el contexto de la salida de informes y el diseño del método de plantillas están explicados en detalle en «Cómo generar informes de Excel».
Cabe señalar que Microsoft 365 tiene una licencia para RPA desatendido (unattended license), pero esta hace posible la ejecución desatendida desde el punto de vista de la licencia, mientras que el comportamiento sigue siendo «AS IS» (“tal cual”) — la postura es que el comportamiento inesperado derivado de un uso fuera del diseño debe absorberlo la propia aplicación.10 Tenga cuidado, porque es un malentendido frecuente pensar que «comprar la licencia convierte la configuración en algo con soporte».
7. Tabla de decisión — seguir usando COM o sustituir por la familia Open XML
Con esto como base, «seguir operando Excel con COM o alejarse de él» se puede organizar en las siguientes tres opciones.
| (1) Seguir usando COM Interop (envolver y disciplinar) | (2) Sustituir por Open XML SDK / ClosedXML | (3) Replantear el propio diseño (Graph, etc.) | |
|---|---|---|---|
| Excel en sí | Necesario (también licencia por cada entorno de ejecución) | No necesario | No necesario |
| Ejecución desatendida / en servidor | Configuración sin soporte5 | Sin problemas (alternativa recomendada)10 | Sin problemas |
| Riesgo de permanencia de procesos | Existe (se gestiona con las técnicas de este artículo) | No existe (no inicia ningún proceso) | No existe |
| Ejecución de macros (VBA) | Posible | No posible (su conservación también depende de la biblioteca — punto 2 más abajo) | No posible |
| Recálculo de fórmulas, impresión, conversión a PDF | Posible | No posible11 | Parcialmente en Graph |
| Interacción con el Excel que el usuario tiene abierto | Posible | No posible | No posible |
| Formato antiguo .xls (BIFF) | Se puede leer y escribir | No posible (solo .xlsx / .xlsm)7 | ── |
| Velocidad de ejecución / paralelismo | Lenta. El paralelismo requiere aislar instancias10 | Rápida. Se puede paralelizar como una biblioteca normal | Depende de la red |
El criterio de decisión se reduce a cuatro preguntas.
- ¿Es necesario interactuar con el Excel que tiene el usuario delante? Una función como «escribir en el libro que el usuario tiene abierto y devolverle el control para que continúe la operación» solo se puede construir con COM Interop. En este caso, la única opción es (1), invirtiendo en la disciplina del capítulo 4. Una aplicación interactiva de escritorio tampoco cae dentro de la configuración sin soporte.
- ¿Se necesitan funciones propias de la aplicación Excel (ejecución de macros, recálculo, impresión, exportación a PDF)? Estas no se pueden sustituir con la familia Open XML.11 La combinación con la ejecución desatendida es el patrón más difícil, así que primero conviene examinar si se puede flexibilizar el requisito (por ejemplo, migrar la lógica de la macro a C#, o escribir directamente los valores ya calculados). Un punto a cuidar es la «conservación» de una plantilla con macros (.xlsm). Con las operaciones de bajo nivel del Open XML SDK se conserva mientras no se toquen las partes del proyecto VBA, pero una biblioteca de alto nivel como ClosedXML carga el libro en un modelo de objetos y reconstruye el paquete al guardarlo, por lo que el proyecto VBA puede perderse. Si va a migrar una plantilla con macros a la familia Open XML, haga obligatorio verificar con la plantilla real que «la macro sigue existiendo y funcionando después de abrir, escribir y guardar»; si no puede garantizarlo, deje ese informe concreto en COM. Para el tratamiento de los activos VBA, el criterio de «Qué es VBA» se puede aplicar tal cual.
- ¿Es ejecución desatendida? Si se ejecuta desde un servicio, el Programador de tareas o una aplicación web, la respuesta por defecto es (2). La mayoría de los requisitos de generación de informes consisten simplemente en «crear un .xlsx con valores y estilos volcados», y eso se completa con ClosedXML.
- ¿Cuál es el formato de entrada y salida? Si hay que procesar tal cual un .xls que llega de un socio comercial, la familia Open XML no sirve. Considere si se puede interponer una conversión a .xlsx en el punto de recepción.
Lo que solemos proponer en proyectos reales es una división del tipo «la generación se hace con ClosedXML, y solo el procesamiento que de verdad requiere el propio Excel se aísla en COM». La generación diaria de cientos de informes se ejecuta en el servidor con ClosedXML, y solo la «actualización mensual del libro con macros» se convierte en un procesamiento COM que el encargado ejecuta con un botón desde su escritorio. Así, COM desaparece del entorno desatendido, y la parte COM que queda, al ser una aplicación interactiva, entra dentro de una configuración con soporte. Es un enfoque más realista que una reescritura completa, y permite eliminar primero las partes de mayor riesgo.
8. Consideraciones en la era de .NET (Core)
A continuación se organizan, dentro de lo que se pudo confirmar, las consideraciones para seguir operando Excel por COM en una aplicación migrada de .NET Framework a .NET (.NET 6/8, etc.).
- La interoperabilidad COM sigue siendo exclusiva de Windows. .NET también funciona en Linux, pero el soporte integrado de interoperabilidad COM está limitado a Windows.12 En un proyecto que incluya operaciones con Excel, especifique explícitamente el target, como
net8.0-windows, y no espere que sea multiplataforma. El hecho de que no se pueda desplegar en un contenedor Linux está directamente relacionado con el criterio de sustitución del capítulo 7 (con ClosedXML sí funciona en un contenedor Linux). - El método de referencia básico es «referencia COM + incrustación de tipos de interoperabilidad». Al agregar la biblioteca de objetos de Microsoft Excel como referencia COM en Visual Studio, por defecto se usa la incrustación de tipos de interoperabilidad (Embed Interop Types). Como solo se incrustan en el propio ensamblado los tipos que realmente se usan, no hace falta distribuir el PIA (ensamblado de interoperabilidad primario) en el entorno de ejecución, y se vuelve más resistente a las diferencias de versión de Office.13
dynamicy los argumentos opcionales se pueden seguir usando. Las funciones de C# orientadas a la interoperabilidad con Office (argumentos con nombre, argumentos opcionales, simplificación de llamadas COM condynamic) siguen siendo compatibles en el .NET actual.13 Sin embargo, escribir condynamichace que los RCW anónimos sean aún más difíciles de ver, así que en el código donde se quiera disciplinar la liberación se recomienda escribir con tipos explícitos.- Cuidado con las API que presuponen una plataforma concreta. Las API relacionadas con COM, como
Marshal.ReleaseComObject, llevan un atributo exclusivo de Windows, y llamarlas desde un proyecto multiplataforma dispara la advertencia del analizador (CA1416). Extraer la operación de Excel a un proyecto independiente facilita su gestión. - El sentido inverso (llamar a .NET desde VBA) es otro asunto. Sigue siendo posible una configuración en la que un DLL de .NET 8 se expone como COM y se usa desde VBA; el procedimiento está resumido en «Cómo usar un DLL de .NET 8 desde VBA con tipado». Si se invierte la configuración —no «manipular Excel desde C#» sino «llamar a lógica de C# desde una macro de Excel»—, la gestión de la vida del proceso queda en manos del propio Excel, y en algunos casos el problema de permanencia desaparece estructuralmente.
En resumen, incluso en la era de .NET (Core), la forma de escribir la operación COM de Excel y sus trampas son prácticamente las mismas que en la era de .NET Framework. Lo que cambia es la necesidad de declarar explícitamente que es exclusivo de Windows, y que la referencia se inclina hacia la incrustación de tipos de interoperabilidad; el análisis del RCW y de los patrones de liberación (capítulos 2 a 5) sigue siendo válido tal cual.
9. Resumen
El problema de la permanencia de EXCEL.EXE deja de ser un fenómeno librado al azar en cuanto se entiende el puente entre dos gestiones de ciclo de vida: «COM vive por el conteo de referencias, y el RCW de .NET muere por el GC». Comprimido en una lista de verificación, queda así.
Quit()no es una liberación. Excel no termina hasta que se devuelven todas las referencias COM que retiene el RCW- Una expresión compuesta con dos o más puntos genera RCW anónimos. Reciba los objetos intermedios en variables
- La liberación se basa en «aislamiento en un método + conjunto de tres pasos del GC»; si hace falta controlar el orden, discipline ReleaseComObject con un envoltorio using. No disperse ReleaseComObject a pelo por el código
- El Kill de seguro identifica solo la instancia propia con
Application.Hwnd→GetWindowThreadProcessId, y se activa únicamente en el esquema «Quit → espera → solo si hay tiempo de espera agotado» - La automatización de Excel en servidor o de forma desatendida es una configuración sin soporte. Migre la generación desatendida de informes a ClosedXML / Open XML SDK, y aísle en COM dentro de un entorno interactivo el procesamiento que necesite macros o recálculo
Si encuentra filas de EXCEL.EXE en el Administrador de tareas, es una señal de que se trata de un problema técnico (capítulos 4 y 5) o de un problema de configuración (capítulos 6 y 7). Si no sabe a cuál de los dos corresponde su código, o desde qué alcance conviene empezar la sustitución, podemos ayudarle desde el inventario del procesamiento.
Artículos relacionados
- Cómo generar informes de Excel - COM/Open XML/plantillas
- Qué es VBA - Limitaciones, futuro, cuándo conviene sustituirlo y patrones de migración realistas
- Qué es COM / ActiveX / OCX - Diferencias y relación explicadas en conjunto
- Guía de revisión de VBA y herramientas internas ante el fin de VBScript
Áreas de consultoría relacionadas
KomuraSoft LLC se encarga de la investigación de problemas en aplicaciones de Windows, incluida la automatización de Excel/Office (permanencia de procesos, cuelgues, bloqueos de archivo), el mantenimiento y la disciplina de los activos COM, y el diseño de la migración del procesamiento de informes hacia bibliotecas de la familia Open XML.
- Consultoría técnica y revisión de diseño
- Aprovechamiento de activos existentes y soporte de migración
- Desarrollo de aplicaciones Windows
- Contacto
Referencias
-
Microsoft Learn, Runtime Callable Wrapper. Sobre el hecho de que se crea un RCW por cada objeto COM dentro del proceso, que cachea el puntero de interfaz y libera la referencia al objeto COM cuando es recolectado por el GC. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Marshal.ReleaseComObject(Object) Method. Sobre el comportamiento de decrementar el conteo de referencias del RCW, el riesgo de InvalidComObjectException, violación de acceso y corrupción de memoria por el uso de un RCW ya liberado, su posición como algo que «solo debe usarse cuando es absolutamente necesario», y la relación con FinalReleaseComObject. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, Application.hWnd property (Excel). Sobre el hecho de que la propiedad Hwnd del objeto Application de Excel devuelve el handle de la ventana de nivel superior. ↩ ↩2 ↩3
-
Microsoft Learn, GetWindowThreadProcessId function (winuser.h). Sobre el hecho de que permite obtener el ID del hilo que creó la ventana indicada y el ID del proceso que creó esa ventana. ↩ ↩2
-
Soporte de Microsoft, Considerations for server-side Automation of Office. Sobre el hecho de que Microsoft no recomienda ni da soporte a la automatización de Office desde aplicaciones o componentes cliente desatendidos y no interactivos (incluyendo ASP, ASP.NET, DCOM y servicios de NT). ↩ ↩2 ↩3
-
Microsoft Learn, Welcome to the Open XML SDK for Office. Sobre el hecho de que Open XML SDK es una biblioteca basada en System.IO.Packaging que manipula el formato de archivo de Office del estándar ECMA-376 / ISO/IEC 29500 mediante clases fuertemente tipadas. ↩ ↩2
-
GitHub, ClosedXML/ClosedXML. Sobre esta biblioteca con licencia MIT, que ofrece una interfaz intuitiva sobre la API de Open XML y permite manejar archivos de Excel 2007+ (.xlsx, .xlsm) sin instalar Excel. ↩ ↩2 ↩3
-
Soporte de Microsoft, Office application does not exit after automation from Visual Studio .NET client. Sobre el hecho de que la causa de que una aplicación de Office no termine aunque se llame a Quit es la referencia que retiene el RCW, y sobre las medidas: declarar cada objeto como una variable nueva (ejemplo de recibir
oExcel.Workbooksen una variable intermedia), llamar a Marshal.ReleaseComObject hasta que devuelva 0, poner las variables en null, llamar a Quit, y, si aun así no termina, llamar a GC.Collect y GC.WaitForPendingFinalizers. ↩ ↩2 -
Microsoft Learn, _Application.AutomationSecurity Property (Microsoft.Office.Interop.Excel). Sobre el modo de seguridad de macros al abrir un archivo desde un programa: que el valor predeterminado al iniciar la aplicación es msoAutomationSecurityLow (todas las macros habilitadas), y que con msoAutomationSecurityForceDisable se pueden deshabilitar todas las macros sin advertencia. ↩
-
Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. Sobre los problemas de la automatización desatendida: UI interactiva, identificación de usuario, diseño de un solo hilo basado en STA, etc.; sobre el hecho de que el comportamiento sigue siendo AS IS incluso bajo la licencia desatendida; y sobre que como alternativa se recomienda Microsoft Graph y la edición directa del formato de archivo Open XML. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Open XML SDK for Office design considerations. Sobre el hecho de que Open XML SDK no es un sustituto del modelo de objetos de Office, y no ofrece comportamientos de aplicación como el recálculo de fórmulas o la actualización de datos, ni funciones de conversión a otros formatos. ↩ ↩2 ↩3
-
Microsoft Learn, Native interoperability ABI support. Sobre el hecho de que el soporte del sistema de interoperabilidad COM integrado está limitado a Windows, y sobre el soporte COM mediante ComWrappers en .NET 5+ y generación de código fuente en .NET 8+. ↩
-
Microsoft Learn, How to access Office interop objects. Sobre la simplificación de la interoperabilidad con Office mediante argumentos con nombre, argumentos opcionales y dynamic, y sobre el hecho de que, en lugar del PIA, la incrustación de tipos de interoperabilidad (Embed Interop Types) es el comportamiento predeterminado. ↩ ↩2
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
¿Las aplicaciones empresariales funcionan en Windows para Arm? — La realidad de la emulación x64 (Prism), las DLL nativas y COM
Respondemos si las aplicaciones empresariales funcionan en Windows para Arm: la emulación x64 (Prism), las capas que no funcionan (contro...
Fecha, hora y zona horaria en aplicaciones empresariales — de las trampas de DateTime al principio de UTC y el diseño de pruebas
Un traslado de servidor desplaza la hora 9 horas: exploramos el origen en el Kind de DateTime, DateTimeOffset, el principio de UTC, TimeZ...
Compatibilidad de WPF con alto DPI ── causas y soluciones del desenfoque pese a que «debería ser resistente al DPI»
WPF es System DPI Aware, pero al moverse a un monitor con otro DPI toda la ventana se difumina y los mapas de bits se ven borrosos. Repas...
Cómo elegir la comunicación entre procesos en Windows — canalizaciones con nombre / TCP / gRPC / memoria compartida / COM: tabla de decisión
Cómo elegir la comunicación entre procesos en Windows: comparamos en una tabla de decisión las canalizaciones con nombre, el TCP local, g...
Diseño de códigos en sistemas empresariales ── Cómo definir códigos de producto y cliente, y el dígito de control
Guía práctica para diseñar códigos de producto y cliente en sistemas empresariales: código significativo frente a secuencial, fórmulas de...
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.
Desarrollo de aplicaciones para Windows
Aplicaciones empresariales, integración de dispositivos y herramientas de comunicación, de los requisitos al desarrollo.
Reutilización y migración de activos existentes
Reutilización y migración de activos COM / ActiveX / OCX y dependencias de 32 o 64 bits.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Por qué EXCEL.EXE no termina aunque se llame a Quit()?
- Porque Quit() no es una liberación, sino solo una solicitud que dice «puedes terminar cuando se liberen todas las referencias». .NET maneja los objetos COM a través de un envoltorio llamado RCW (Runtime Callable Wrapper), que retiene la referencia COM hasta que el propio RCW es recolectado por el GC (o liberado explícitamente). Mientras queden referencias, Excel espera fielmente. Como el momento de la liberación depende del GC y es indeterminado, la falta de reproducibilidad de «desaparece en la máquina de desarrollo pero permanece en producción» también se explica por esa variabilidad en el momento del GC.
- ¿Qué es la «regla de los dos puntos» en la operación COM de Excel?
- Es una regla práctica que dice: no encadenar dos o más puntos sobre un objeto COM, y recibir cada objeto intermedio en una variable. Una expresión compuesta como book.Worksheets[1].Range["A1"] genera en una sola línea varios RCW anónimos: Worksheets (la colección), Worksheets[1] (la hoja) y Range["A1"] (el rango). Como no están asignados a ninguna variable, no hay forma de llamar a Marshal.ReleaseComObject sobre ellos, y se convierten en el principal culpable de las fugas de liberación. Hay que tener cuidado porque también se generan RCW anónimos de la misma forma en un foreach, al descartar el resultado dentro de una expresión condicional, o como argumento de una expresión compuesta.
- ¿Cómo debo escribir el código para garantizar que EXCEL.EXE termine?
- Lo recomendable es aislar por completo el procesamiento que toca Excel en un único método y, después de salir de ese método, ejecutar el conjunto de tres pasos GC.Collect → GC.WaitForPendingFinalizers → GC.Collect. Como el RCW libera la referencia COM precisamente cuando el GC lo recolecta, con este esquema incluso los RCW anónimos se recolectan aunque se rompa la regla de los dos puntos. También existe la práctica de aplicar Marshal.ReleaseComObject a todos los objetos, pero la documentación oficial la sitúa como algo que «solo debe usarse cuando es absolutamente necesario», ya que el uso indebido de un RCW ya liberado provoca InvalidComObjectException o corrupción de memoria. Solo cuando se necesita controlar el orden de liberación conviene introducir un envoltorio disciplinado con using.
- ¿Se puede automatizar Excel desde un servidor o un proceso por lotes?
- Microsoft afirma explícitamente que no recomienda ni da soporte a la automatización de Office desde aplicaciones o componentes cliente desatendidos y no interactivos (incluyendo ASP.NET, DCOM y servicios de NT). Office está diseñado bajo la premisa de que existe un usuario interactivo, por lo que en un servicio o en IIS se producen bloqueos por espera de cuadros de diálogo y una acumulación indefinida de procesos. Para la generación desatendida de informes, la alternativa recomendada es sustituir Excel por Open XML SDK o ClosedXML, que manipulan el archivo directamente sin iniciar Excel. Un enfoque realista es dividir el trabajo: aislar en COM, dentro de un entorno interactivo, únicamente el procesamiento que realmente necesita funciones propias de la aplicación Excel, como la ejecución de macros o el recálculo.
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.