Est-il possible d'intercepter (ou de connaître) le comptage de référence COM sur les objets CLR exposés à COM

j'ai reformulé cette question.

quand les objets .net sont exposés aux Clients COM via COM iterop, une CCW (COM Callable Wrapper) est créé, cela se situe entre le Client COM et L'objet Managed .net.

dans le monde COM, les objets comptent le nombre de références que les autres objets ont à eux. Les objets sont supprimés/libérés / collectés lorsque le nombre de référence passe à zéro. Cela signifie que la fin de L'objet COM est déterministe (nous utilisons L'utilisation de/IDispose dans .net pour la terminaison déterministe, les finaliseurs d'objet sont non déterministes).

chaque CCW est un objet COM, et il est une référence comptée comme n'importe quel autre objet COM. Lorsque la CCW décède (le compte de référence passe à zéro), le GC ne sera pas en mesure de trouver l'objet CLR que la CCW a enveloppé, et l'objet CLR est admissible à la collecte. Les jours heureux, tout va bien avec le monde.

ce que je voudrais faire c'est attraper quand la CCW meurt (i.e. quand son le compte de référence va à zéro), et d'une manière ou d'une autre signalez ceci à l'objet CLR (par exemple en appelant une méthode Dispose sur l'objet géré).

Donc, est-il possible de savoir quand le compteur de référence d'un COM Callable Wrapper pour une classe CLR passe à zéro?

et/ou

Est-il possible de me fournir ma mise en œuvre de AddRef Et ReleaseRef pour CCWs .net?

si ce n'est pas le cas, la solution consiste à les mettre en oeuvre. Dll ATL (je n'ai pas besoin d'aide avec ATL, merci). Ce ne serait pas de la science pure, mais je suis réticent à le faire car je suis le seul développeur en interne avec N'importe quel C++ du monde réel, ou N'importe quel ATL.

Background

Je suis en train de réécrire quelques vieux DLLs ActiveX VB6 dans .net (C# pour être exact, mais c'est plus un problème d'interop.net / COM qu'un problème de C#). Certains des anciens objets VB6 dépendent du comptage de référence pour effectuer des actions lorsque l'objet se termine (voir explication de comptage de référence ci-dessus). Ces DLL ne contiennent pas de logique commerciale importante, ce sont des utilitaires et des fonctions d'aide que nous fournissons aux clients qui s'intègrent avec nous en utilisant VBScript.

Ce que je ne suis pas en train de faire

  • nombre de références objets .net à la place de L'utilisation du collecteur D'ordures. Je suis très satisfait du GC, mon le problème n'est pas avec le GC.
  • utiliser des finaliseurs d'objets. Les finalisateurs sont non déterministe, dans ce cas, j' fin déterministe nécessaire (comme l'Utilisation de/IDispose idiome .net)
  • mise en Œuvre IUnknown dans le C++

    Si je dois suivre la route du c++, j'utiliserai ATL, merci.
  • résolvez ceci en utilisant Vb6, ou en réutilisant le VB6 objects. L'ensemble de point de cet exercice est d'enlever nos la dépendance sur Vb6.

Merci

BW

La Réponse Acceptée

Les gens un mille merci à Steve Steiner, qui a trouvé la seule (peut-être réalisable) réponse basée sur. net, et Earwicker, qui a trouvé une solution très simple pour L'ATL.

cependant la réponse acceptée va à Bigtoe, qui suggère d'envelopper les objets .net dans des objets VbScript (ce que je n'avais pas considéré comme honnête), fournissant effectivement une solution VbScript simple à un problème VbScript.

Merci à tous.

17
demandé sur Binary Worrier 2010-02-08 19:38:46

10 réponses

OK les amis, voici une autre tentative. Vous pouvez en fait utiliser "Windows Script Components" pour envelopper vos objets.net COM et obtenir la finalisation de cette façon. Voici un exemple complet à L'aide d'une calculatrice simple .NET qui peut ajouter des valeurs. Je suis sûr que vous obtiendrez le concept à partir de là, ce qui évite totalement les problèmes VB-Runtime, ATL et utilise L'hôte de script Windows qui est disponible sur chaque plate-forme Win64/Win64 majeure.

j'ai créé une classe COM .NET simple appelée calculatrice dans un des espaces-noms appelés DemoLib. Notez que ceci implémente IDisposable où j'ai mis quelque chose à l'écran pour montrer qu'il s'est terminé. Je m'en tiens totalement à vb ici dans .NET et script pour garder les choses simples, mais la partie .NET peut être en C# etc. Lorsque vous enregistrez ce fichier, vous devez l'enregistrer avec regsvr32, il devra être enregistré comme quelque chose comme CalculatorLib.la csm.

<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

ensuite je crée un composant de Script Windows appelé Calculatrice.Lib qui a un seul méthode qui renvoie une classe COM VB-Script qui expose la bibliothèque de mathématiques .NET. Ici je fais apparaître quelque chose sur l'écran pendant la Construction et la Destruction, notez dans la Destruction que nous appelons la méthode Dispose dans la bibliothèque .NET pour libérer des ressources là-bas. Notez l'utilisation de la fonction Lib() pour retourner la calculatrice.net Com à l'appelant.

<?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>

enfin pour tout relier voici un exemple de script VB où vous obtenez des dialogues montrant la création, le calcul, disposer être appelé dans la bibliothèque .NET et se terminer finalement dans le composant COM exposant le composant .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.")
5
répondu Bigtoe 2010-02-16 16:42:50

je me rends compte que c'est une question un peu ancienne, mais j'ai obtenu la demande réelle de travailler quelque temps en arrière.

ce qu'il fait, c'est remplacer la version dans le(S) VTBL (s) de l'objet créé par une implémentation personnalisée qui appelle Dispose lorsque toutes les références ont été publiées. Notez qu'il n'y a aucune garantie à ce que cela fonctionne toujours. L'hypothèse principale est que toutes les méthodes de libération sur toutes les interfaces de la CCW standard sont la même méthode.

Utilisez à vos risques et périls. :)

/// <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);
    }
}
5
répondu Darren Clark 2011-08-01 22:18:12

je n'ai pas vérifié cela, mais voici ce que je voudrais essayer:

tout d'Abord, voici un article du Blog CBrumme à propos de la mise en œuvre par défaut D'IMarshal par le clr. Si vos utilitaires sont utilisés dans tous les appartements COM, vous n'obtiendrez pas un comportement com correct d'un port direct de VB6 vers le CLR. Les objets Com mis en œuvre par le CLR agissent comme s'ils regroupaient le modèle de marshaller fileté libre plutôt que le modèle de VB6 fileté appartement exposé.

vous pouvez mettre en oeuvre IMarshal (sur la classe clr vous exposez comme un objet com). Ma compréhension est que vous permettra de contrôler la création du proxy COM (pas le proxy interop). Je pense que cela vous permettra de piéger les appels de libération dans l'objet que vous avez retourné depuis UnmarshalInterface et de renvoyer le signal à votre objet original. J'envelopperais le marshaller standard (par exemple pinvoke CoGetStandardMarshaler) et lui transmettre tous les appels. Je crois que cet objet aura une vie liée à la vie de la CCAC.

nouveau ... c'est ce que j'essaierais si je devais le résoudre en C#.

d'un autre côté, Est-ce que ce genre de solution serait vraiment plus facile à mettre en œuvre qu'en ATL? Ce n'est pas parce que la partie magique est écrite en C# que la solution est simple. Si ce que je propose ci-dessus résout le problème, vous aurez besoin d'écrire un très grand commentaire expliquant ce qui se passait.

4
répondu Steve Steiner 2010-02-14 03:30:53

j'ai eu du mal avec cela aussi, pour essayer d'obtenir la durée de vie du serveur correcte pour mon gestionnaire de prévisualisation, comme décrit ici: Afficher Les Données De Votre Chemin Avec Notre Géré Preview Handler

j'avais besoin de le mettre dans un serveur de processus, et soudainement j'ai eu des problèmes de contrôle à vie.

La façon d'entrer dans un processus serveur qui est décrit ici, pour toute personne intéressée: RegistrationSrvices.Type d'enregistrement pour la communauté des clients contenu ce qui implique que vous pouvez être capable de le faire en implorant IDispose, mais cela n'a pas fonctionné.

j'ai essayé d'implémenter un finaliseur, ce qui a finalement provoqué la libération de l'objet, mais à cause du modèle d'utilisation du serveur appelant mon objet, cela signifiait que mon serveur était resté pour toujours. J'ai aussi essayé d'enlever un objet au travail, et après une nuit de sommeil, forçant une collecte des ordures, mais c'était vraiment le bordel.

au lieu de cela, il est venu à la libération d'accrochage (et AddRef parce que la valeur de retour de Release ne pouvait pas être fiable).

(trouvé via ce post: http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675)

voici ce que j'ai fait dans le constructeur de mon objet:

//  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; 
}
3
répondu Anthony Wieser 2011-05-23 20:35:18