c# .net com-interop reference-counting

c# - ¿Es posible interceptar(o tener en cuenta) la referencia de COM contando con objetos CLR expuestos a COM?



.net com-interop (10)

¿Por qué no cambiar de paradigma? ¿Qué hay de crear su propio agregado alrededor de métodos de notificación expuestos y extenderse? Incluso se puede hacer en .Net no solo por ATL.

EDITADO : Aquí hay un enlace que puede describirse de otra manera ( http://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx ). Pero los siguientes pasos explican mi idea anterior.

Crear crear una nueva clase .Net que implemente su interfaz heredada (ILegacy) y una nueva interfaz (ISendNotify) con un solo método:

interface ISendNotify { void SetOnDestroy(IMyListener ); } class MyWrapper : ILegacy, ISendNotify, IDisposable{ ...

Dentro de MyClass, cree una instancia de su objeto heredado real y delegue todas las llamadas de MyClass a esta instancia. Esta es una agregación. Así que la vida útil del agregado ahora depende de MyClass. Ya que MyClass es IDisposable ahora puede interceptar cuando se borra la instancia, por lo que puede enviar una notificación por IMyListener

EDIT2 : Tomado allí ( http://vb.mvps.org/hardcore/html/countingreferences.htm ) la implementación más simple de IUnknown con el evento de envío

Class MyRewritten ... Implements IUnknown Implements ILegacy ... Sub IUnknown_AddRef() c = c + 1 End Sub Sub IUnknown_Release() c = c - 1 If c = 0 Then RaiseEvent Me.Class_Terminate Erase Me End If End Sub

He reformulado esta pregunta.

Cuando los objetos .net se exponen a los Clientes COM a través de COM iterop, se crea un CCW ( COM Callable Wrapper ), que se encuentra entre el Cliente COM y el objeto .net administrado.

En el mundo COM, los objetos cuentan la cantidad de referencias que otros objetos tienen para ello. Los objetos se eliminan / liberan / recolectan cuando el recuento de referencia va a cero. Esto significa que la terminación del objeto COM es determinista (usamos Using / IDispose en .net para la terminación determinista, los finalizadores de objetos no son deterministas).

Cada CCW es un objeto COM y su referencia se cuenta como cualquier otro objeto COM. Cuando el CCW muere (el recuento de referencia va a cero), el GC no podrá encontrar el objeto CLR en el CCW envuelto, y el objeto CLR es elegible para la recopilación. Días felices, todo está bien con el mundo.

Lo que me gustaría hacer es capturar cuando el CCW muere (es decir, cuando su recuento de referencia se pone a cero), y de alguna manera señalarlo al objeto CLR (por ejemplo, llamando a un método Dispose en el objeto administrado).

Entonces, ¿es posible saber cuándo el recuento de referencia de un COM Callable Wrapper para una clase CLR va a cero?
y / o
¿Es posible proporcionar mi implementación de AddRef y ReleaseRef para CCW en .net?

Si no, la alternativa es implementar estos DLL en ATL (no necesito ninguna ayuda con ATL, gracias). No sería ciencia espacial, pero me resisto a hacerlo ya que soy el único desarrollador interno con C ++ en el mundo real, o cualquier ATL.

Fondo
Estoy reescribiendo algunas DLL de ActiveX VB6 antiguas en .net (C # para ser exactos, pero esto es más un problema de interoperabilidad .net / COM en lugar de un problema de C #). Algunos de los objetos VB6 antiguos dependen del conteo de referencias para realizar acciones cuando el objeto termina (consulte la explicación del conteo de referencias más arriba). Estas DLL no contienen una lógica empresarial importante, son utilidades y funciones de ayuda que proporcionamos a los clientes que se integran con nosotros mediante VBScript.

Lo que no estoy tratando de hacer

  • Referencia contar objetos .net en lugar de usar el recolector de basura. Estoy bastante contento con el GC, mi problema no es con el GC.
  • Utilizar finalizadores de objetos. Los finalizadores no son deterministas, en este caso necesito una terminación determinista (como el lenguaje de uso / IDispose en .net)
  • Implementar IUnknown en C ++ no administrado
    Si tengo que ir a la ruta de C ++ usaré ATL, gracias.
  • Resuelva esto utilizando Vb6, o reutilizando los objetos VB6. El objetivo de este ejercicio es eliminar nuestra dependencia de compilación en Vb6.

Gracias
BW

La respuesta aceptada
Muchísimas gracias a Steve Steiner , a quien se le ocurrió la única respuesta basada en .net (posiblemente viable), y Earwicker , a quien se le ocurrió una solución ATL muy simple.

Sin embargo, la respuesta aceptada se dirige a Bigtoe , quien sugiere envolver los objetos .net en objetos VbScript (que no consideré honestos), proporcionando efectivamente una solución VbScript simple para un problema de VbScript.

Gracias a todos.


.Net Framework funciona de manera diferente, ver:
.NET Framework proporciona técnicas de administración de memoria que difieren de la forma en que funcionaba la administración de memoria en un mundo basado en COM. La gestión de memoria en COM fue a través del conteo de referencias. .NET proporciona una técnica de administración de memoria automática que involucra el rastreo de referencias. En este artículo, echaremos un vistazo a la técnica de recolección de basura utilizada por el CLR de Common Language Runtime.

nada que hacer

[EDITADO] más una ronda ...

Eche un vistazo a esta alternativa Importar una biblioteca de tipos como un ensamblaje
Como usted mismo dijo utilizando CCW, puede acceder al código de referencia en la forma tradicional de COM .

[EDITADO] La persistencia es una virtud.
¿Sabes WinAPIOverride32 ? Con él podrás capturar y estudiar cómo funciona. Otra herramienta que puede ayudar es Deviare COM Spy Console .
Esto no será fácil.
Buena suerte.


Desde .NET, solicite un IUnknown en el objeto. Llame a AddRef (), luego suelte (). Luego tome el valor de retorno de AddRef () y ejecútelo.


Me doy cuenta de que esta es una pregunta un tanto antigua, pero recibí la solicitud real para trabajar hace algún tiempo.

Lo que hace es reemplazar el lanzamiento en los VTBL (s) del objeto creado con una implementación personalizada que llama a Desechar cuando se hayan liberado todas las referencias. Tenga en cuenta que no hay garantías de que esto siempre funcionará. El supuesto principal es que todos los métodos de lanzamiento en todas las interfaces de la CCW estándar son el mismo método.

Úselo bajo su propio riesgo. :)

/// <summary> /// I base class to provide a mechanism where <see cref="IDisposable.Dispose"/> /// will be called when the last reference count is released. /// /// </summary> public abstract class DisposableComObject: IDisposable { #region Release Handler, ugly, do not look //You were warned. //This code is to enable us to call IDisposable.Dispose when the last ref count is released. //It relies on one things being true: // 1. That all COM Callable Wrappers use the same implementation of IUnknown. //What Release() looks like with an explit "this". private delegate int ReleaseDelegate(IntPtr unk); //GetFunctionPointerForDelegate does NOT prevent GC ofthe Delegate object, so we''ll keep a reference to it so it''s not GC''d. //That would be "bad". private static ReleaseDelegate myRelease = new ReleaseDelegate(Release); //This is the actual address of the Release function, so it can be called by unmanaged code. private static IntPtr myReleaseAddress = Marshal.GetFunctionPointerForDelegate(myRelease); //Get a Delegate that references IUnknown.Release in the CCW. //This is where we assume that all CCWs use the same IUnknown (or at least the same Release), since //we''re getting the address of the Release method for a basic object. private static ReleaseDelegate unkRelease = GetUnkRelease(); private static ReleaseDelegate GetUnkRelease() { object test = new object(); IntPtr unk = Marshal.GetIUnknownForObject(test); try { IntPtr vtbl = Marshal.ReadIntPtr(unk); IntPtr releaseAddress = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size); return (ReleaseDelegate)Marshal.GetDelegateForFunctionPointer(releaseAddress, typeof(ReleaseDelegate)); } finally { Marshal.Release(unk); } } //Given an interface pointer, this will replace the address of Release in the vtable //with our own. Yes, I know. private static void HookReleaseForPtr(IntPtr ptr) { IntPtr vtbl = Marshal.ReadIntPtr(ptr); IntPtr releaseAddress = Marshal.ReadIntPtr(vtbl, 2 * IntPtr.Size); Marshal.WriteIntPtr(vtbl, 2 * IntPtr.Size, myReleaseAddress); } //Go and replace the address of CCW Release with the address of our Release //in all the COM visible vtables. private static void AddDisposeHandler(object o) { //Only bother if it is actually useful to hook Release to call Dispose if (Marshal.IsTypeVisibleFromCom(o.GetType()) && o is IDisposable) { //IUnknown has its very own vtable. IntPtr comInterface = Marshal.GetIUnknownForObject(o); try { HookReleaseForPtr(comInterface); } finally { Marshal.Release(comInterface); } //Walk the COM-Visible interfaces implemented //Note that while these have their own vtables, the function address of Release //is the same. At least in all observed cases it''s the same, a check could be added here to //make sure the function pointer we''re replacing is the one we read from GetIUnknownForObject(object) //during initialization foreach (Type intf in o.GetType().GetInterfaces()) { if (Marshal.IsTypeVisibleFromCom(intf)) { comInterface = Marshal.GetComInterfaceForObject(o, intf); try { HookReleaseForPtr(comInterface); } finally { Marshal.Release(comInterface); } } } } } //Our own release. We will call the CCW Release, and then if our refCount hits 0 we will call Dispose. //Note that is really a method int IUnknown.Release. Our first parameter is our this pointer. private static int Release(IntPtr unk) { int refCount = unkRelease(unk); if (refCount == 0) { //This is us, so we know the interface is implemented ((IDisposable)Marshal.GetObjectForIUnknown(unk)).Dispose(); } return refCount; } #endregion /// <summary> /// Creates a new <see cref="DisposableComObject"/> /// </summary> protected DisposableComObject() { AddDisposeHandler(this); } /// <summary> /// Calls <see cref="Dispose"/> with false. /// </summary> ~DisposableComObject() { Dispose(false); } /// <summary> /// Override to dispose the object, called when ref count hits or during GC. /// </summary> /// <param name="disposing"><b>true</b> if called because of a 0 refcount</param> protected virtual void Dispose(bool disposing) { } void IDisposable.Dispose() { Dispose(true); GC.SuppressFinalize(this); } }


No he verificado esto, pero esto es lo que intentaría:

Primero, aquí hay un artículo del blog de CBrumme sobre la implementación predeterminada de IMarshal por parte del cliente. Si sus utilidades se utilizan en apartamentos COM, no obtendrá el comportamiento adecuado de com desde un puerto directo de VB6 a CLR. Los objetos Com implementados por el CLR actúan como si agregaran al agente de subprocesos con hilos gratuitos en lugar del modelo con hilos de apartamento que VB6 expuso.

Puede implementar IMarshal (en la clase clr que está exponiendo como un objeto com). Mi entendimiento es que le permitirá controlar la creación del proxy COM (no el proxy de interoperabilidad). Creo que esto le permitirá interceptar las llamadas de liberación en el objeto que devolvió de UnmarshalInterface y enviar una señal al objeto original. Envolvería el contador de CoGetStandardMarshaler estándar (por ejemplo, Pinvoke CoGetStandardMarshaler ) y le reenviaría todas las llamadas. Creo que ese objeto tendrá una vida útil ligada a la vida útil de la CCW.

otra vez ... esto es lo que intentaría si tuviera que resolverlo en C #.

Por otro lado, ¿este tipo de solución sería realmente más fácil que la implementación en ATL? El hecho de que la parte mágica esté escrita en C # no simplifica la solución. Si lo que propongo anteriormente resuelve el problema, deberás escribir un comentario realmente grande que explique lo que estaba sucediendo.


OK amigos, aquí hay otro intento de hacerlo. Realmente puede usar "Componentes de Windows Script" para envolver sus objetos COM de .NET y obtener la finalización de esa manera. Aquí hay una muestra completa utilizando una simple calculadora .NET que puede agregar valores. Estoy seguro de que obtendrá el concepto a partir de ahí, esto evita totalmente los problemas de VB-Runtime, ATL y utiliza el Windows Scripting Host, que está disponible en todas las plataformas principales WIN32 / WIN64.

Creé una simple clase COM .NET llamada Calculadora en un espacio de nombres llamado DemoLib. Tenga en cuenta que esto implementa IDisposable donde, para fines de demostración, coloco algo en la pantalla para indicar que ha finalizado. Me quedo totalmente en vb aquí en .NET y en el script para simplificar las cosas, pero la parte de .NET puede estar en C #, etc. Como algo así como CalculatorLib.wsc.

<ComClass(Calculator.ClassId, Calculator.InterfaceId, Calculator.EventsId)> _ Public Class Calculator Implements IDisposable #Region "COM GUIDs" '' These GUIDs provide the COM identity for this class '' and its COM interfaces. If you change them, existing '' clients will no longer be able to access the class. Public Const ClassId As String = "68b420b3-3aa2-404a-a2d5-fa7497ad0ebc" Public Const InterfaceId As String = "0da9ab1a-176f-49c4-9334-286a3ad54353" Public Const EventsId As String = "ce93112f-d45e-41ba-86a0-c7d5a915a2c9" #End Region '' A creatable COM class must have a Public Sub New() '' with no parameters, otherwise, the class will not be '' registered in the COM registry and cannot be created '' via CreateObject. Public Sub New() MyBase.New() End Sub Public Function Add(ByVal x As Double, ByVal y As Double) As Double Return x + y End Function Private disposedValue As Boolean = False '' To detect redundant calls '' IDisposable Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not Me.disposedValue Then If disposing Then MsgBox("Disposed called on .NET COM Calculator.") End If End If Me.disposedValue = True End Sub #Region " IDisposable Support " '' This code added by Visual Basic to correctly implement the disposable pattern. Public Sub Dispose() Implements IDisposable.Dispose '' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above. Dispose(True) GC.SuppressFinalize(Me) End Sub #End Region End Class

A continuación, creo un componente de Windows Script llamado Calculator.Lib que tiene un método único que devuelve una clase COM de VB-Script que expone la biblioteca matemática .NET. Aquí muestro algo en la pantalla durante Construcción y Destrucción, nota en la Destrucción que llamamos el método Dispose en la biblioteca .NET para liberar recursos allí. Tenga en cuenta el uso de la función Lib () para devolver la .NET Com Calculator a la persona que llama.

<?xml version="1.0"?> <component> <?component error="true" debug="true"?> <registration description="Demo Math Library Script" progid="Calculator.Lib" version="1.00" classid="{0df54960-4639-496a-a5dd-a9abf1154772}" > </registration> <public> <method name="GetMathLibrary"> </method> </public> <script language="VBScript"> <![CDATA[ Option Explicit ''----------------------------------------------------------------------------------------------------- '' public Function to return back a logger. ''----------------------------------------------------------------------------------------------------- function GetMathLibrary() Set GetMathLibrary = New MathLibrary end function Class MathLibrary private dotNetMatFunctionLib private sub class_initialize() MsgBox "Created." Set dotNetMatFunctionLib = CreateObject("DemoLib.Calculator") end sub private sub class_terminate() dotNetMatFunctionLib.Dispose() Set dotNetMatFunctionLib = nothing MsgBox "Terminated." end sub public function Lib() Set Lib = dotNetMatFunctionLib End function end class ]]> </script> </component>

Finalmente, para unirlo todo, aquí está la secuencia de comandos de VB de muestra, donde se muestran los diálogos que muestran la creación, el cálculo, la disposición en la biblioteca .NET y, finalmente, la terminación en el componente COM que expone el componente .NET.

dim comWrapper dim vbsCalculator set comWrapper = CreateObject("Calculator.Lib") set vbsCalculator = comWrapper.GetMathLibrary() msgbox "10 + 10 = " & vbsCalculator.lib.Add(10, 10) msgbox "20 + 20 = " & vbsCalculator.lib.Add(20, 20) set vbsCalculator = nothing MsgBox("Dispose & Terminate should have been called before here.")


Que yo sepa, el GC ya proporciona soporte para lo que está tratando de hacer. Se llama Finalización. En un mundo puramente gestionado, la mejor práctica es evitar la Finalización, ya que tiene algunos efectos secundarios que pueden afectar negativamente el rendimiento y el funcionamiento del GC. La interfaz IDisposable proporciona una forma limpia y administrada de omitir la finalización de objetos y de limpiar los recursos administrados y no administrados desde el código administrado.

En su caso, debe iniciar la limpieza de un recurso administrado una vez que se hayan liberado todas las referencias no administradas . La finalización debe sobresalir en resolver su problema aquí. El GC finalizará un objeto, siempre, si hay un finalizador presente, independientemente de cómo se liberaron las últimas referencias a un objeto finalizable. Si implementa un finalizador en su tipo .NET (solo implementa un destructor), entonces el GC lo colocará en la cola de finalización. Una vez que se complete el ciclo de recolección de GC, procesará la cola de finalización. Cualquier trabajo de limpieza que realice en su destructor ocurrirá una vez que se procese la cola de finalización.

Debe tenerse en cuenta que si su tipo .NET finalizable contiene referencias a otros objetos .NET que a su vez requieren finalización, puede invocar una colección GC extensa, o algunos de los objetos pueden sobrevivir durante más tiempo de lo que serían sin la finalización (lo que significaría sobreviven a una colección y llegan a la siguiente generación, que se recolecta con menos frecuencia.) Sin embargo, si el trabajo de limpieza de sus objetos .NET que usan CCW no es sensible al tiempo de ninguna manera, y el uso de la memoria no es un gran problema, algunos extra la vida no debería importar Debe tenerse en cuenta que los objetos finalizables deben crearse con cuidado, y minimizar o eliminar cualquier referencia de nivel de instancia de clase a otros objetos puede mejorar la administración general de la memoria a través del GC.

Puede leer más sobre la finalización en este artículo: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx . Si bien es un artículo bastante antiguo de cuando se lanzó .NET 1.0 por primera vez, la arquitectura fundamental del GC aún no ha cambiado (los primeros cambios significativos en el GC llegarán con .NET 4.0, sin embargo, están más relacionados con ejecución simultánea de GC sin congelar los subprocesos de la aplicación que los cambios en su operación fundamental.)


Que yo sepa, la mejor cobertura de este tema se encuentra en el manual de interoperabilidad de .NET y COM de Alan Gordon, y ese enlace debe ir a la página correspondiente en Google Books. (Desafortunadamente no lo tengo, me fui por el libro de Troelsen ).

La guía allí implica que no hay una forma bien definida de engancharse al conteo de Referencias / Referencias en la CCW. En su lugar, la sugerencia es que haga que su clase de C # sea desechable y aliente a sus clientes COM (en su caso, los autores de VBScript) a llamar a Dispose cuando quieran que se produzca una finalización determinista.

Pero felizmente hay una laguna para usted porque sus clientes son clientes COM de enlace tardío, porque VBScript usa IDispatch para hacer todas las llamadas a objetos.

Supongamos que sus clases de C # fueron expuestas a través de COM. Haz que funcione primero.

Ahora, en ATL / C ++, cree una clase de envoltorio, utilizando el asistente de objetos simples de ATL, y en la página de opciones, elija Interfaz: Personalizada en lugar de Doble. Esto detiene al asistente poniendo su propio soporte de IDispatch .

En el constructor de la clase, use CoCreateInstance para crear una instancia de su clase C #. Consúltelo por IDispatch y mantenga ese puntero en un miembro.

Agregue IDispatch a la lista de herencia de la clase contenedora y reenvíe los cuatro métodos de IDispatch directamente al puntero que escondió en el constructor.

En la FinalRelease de la envoltura, use la técnica de enlace tardío ( Invoke ) para llamar al método Dispose del objeto C #, como se describe en el libro de Alan Gordon (en las páginas que he enlazado anteriormente).

Así que ahora sus clientes de VBScript están hablando a través de CCW a la clase C #, pero pueden interceptar la versión final y Dispose método Dispose .

Haga que su biblioteca ATL exponga un envoltorio separado para cada clase "real" de C #. Probablemente querrá usar herencia o plantillas para obtener una buena reutilización del código aquí. Cada clase de C # que admita solo debe requerir un par de líneas en el código de ajuste ATL.


Supongo que la razón para que esto no sea posible es que un refcount de 0 no significa que el objeto no esté en uso, ya que es posible que tenga un gráfico de llamadas como

VB_Object | V | Managed1 -<- Managed2

en cuyo caso, el objeto Managed1 aún está en uso, incluso si el objeto VB descarta su referencia y, por lo tanto, su referencia es 0.

Si realmente necesita hacer lo que dice, creo que podría crear clases de envoltorio en C ++ no administrado, que invoca el método Dispose cuando el refcount cae a 0. Estas clases probablemente podrían estar codificadas a partir de metadatos, pero no tengo experiencia alguna. en cómo hacer implementar este tipo de cosas.


También he estado luchando con esto, para intentar corregir la vida útil del servidor para mi controlador de vista previa, como se describe aquí: Ver datos a su manera con nuestro marco de manejo de vista previa administrado

Necesitaba ponerlo en un servidor fuera de proceso, y de repente tuve problemas de control de por vida.

La forma de acceder a un servidor fuera de proceso se describe aquí para cualquier persona interesada: RegistrationSrvices.RegisterTypeForComClients, el contenido de la comunidad que implica que puede hacerlo implementando IDispose, pero eso no funcionó.

Intenté implementar un finalizador, que eventualmente hizo que el objeto fuera liberado, pero debido al patrón de uso del servidor que llama a mi objeto, significaba que mi servidor se quedaba para siempre. También intenté escindir un elemento de trabajo, y después de dormir, forzando una recolección de basura, pero eso era realmente desordenado.

En su lugar, se redujo a conectar la versión (y AddRef porque el valor de retorno de la versión no era confiable).

(Encontrado a través de este post: http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675 )

Esto es lo que hice en el constructor de mi objeto:

// Get the CCW for the object _myUnknown = Marshal.GetIUnknownForObject(this); IntPtr _vtable = Marshal.ReadIntPtr(_myUnknown); // read out the AddRef/Release implementation _CCWAddRef = (OverrideAddRef) Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(_vtable, 1 * IntPtr.Size), typeof(OverrideAddRef)); _CCWRelease = (OverrideRelease) Marshal.GetDelegateForFunctionPointer(Marshal.ReadIntPtr(_vtable, 2 * IntPtr.Size), typeof(OverrideRelease)); _MyRelease = new OverrideRelease(NewRelease); _MyAddRef = new OverrideAddRef(NewAddRef); Marshal.WriteIntPtr(_vtable, 1 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate(_MyAddRef)); Marshal.WriteIntPtr(_vtable, 2 * IntPtr.Size, Marshal.GetFunctionPointerForDelegate(_MyRelease)); and the declarations: int _refCount; delegate int OverrideAddRef(IntPtr pUnknown); OverrideAddRef _CCWAddRef; OverrideAddRef _MyAddRef; delegate int OverrideRelease(IntPtr pUnknown); OverrideRelease _CCWRelease; OverrideRelease _MyRelease; IntPtr _myUnknown; protected int NewAddRef(IntPtr pUnknown) { Interlocked.Increment(ref _refCount); return _CCWAddRef(pUnknown); } protected int NewRelease(IntPtr pUnknown) { int ret = _CCWRelease(pUnknown); if (Interlocked.Decrement(ref _refCount) == 0) { ret = _CCWRelease(pUnknown); ComServer.Unlock(); } return ret; }