Подтвердить что ты не робот

Можно ли перехватывать (или быть в курсе) COM-ссылкой, подсчитывая объекты CLR, подверженные COM

Я перефразировал этот вопрос.

Когда объекты .net подвергаются COM-клиентам через COM iterop, создается CCW (COM Callable Wrapper), это находится между COM-клиент и управляемый .net-объект.

В мире COM объекты сохраняют количество ссылок, которое ему принадлежат другие объекты. Объекты удаляются/освобождаются/собираются, когда этот счетчик ссылок переходит в нуль. Это означает, что окончание COM-объекта является детерминированным (мы используем использование /IDispose в .net для детерминированного завершения, финализаторы объектов не детерминированы).

Каждый CCW является COM-объектом, и эта ссылка считается как любой другой COM-объект. Когда CCW умирает (счетчик ссылок идет до нуля), GC не сможет найти объект CLR, который обернут CCW, и объект CLR имеет право на сбор. Счастливые дни, все хорошо с миром.

То, что я хотел бы сделать, - это поймать, когда CCW умирает (т.е. когда его счетчик ссылок идет на ноль) и каким-то образом сигнализирует об этом объекту CLR (например, вызывая метод Dispose на управляемом объекте).

Итак, можно ли узнать, когда счетчик ссылок COM Callable Wrapper для класса CLR переходит в нуль?
и/или
Возможно ли предоставить мою реализацию AddRef и ReleaseRef для CCW в .net?

Если нет альтернативы, чтобы реализовать эти DLL в ATL (мне не нужна помощь с ATL, спасибо). Это не будет наука о ракете, но я неохотно это делаю, поскольку я единственный разработчик в доме с любым реальным С++ или любым ATL.

Фон
Я переписываю некоторые старые VB6 ActiveX DLL в .net(С#, если быть точным, но это скорее проблема с .net/COM-взаимодействием, а не с проблемой С#). Некоторые из старых объектов VB6 зависят от подсчета ссылок для выполнения действий, когда объект завершается (см. Объяснение подсчета ссылок выше). Эти DLL не содержат важной бизнес-логики, это утилиты и вспомогательные функции, которые мы предоставляем клиентам, которые интегрируются с нами с помощью VBScript.

Что я не пытаюсь сделать

  • Вместо этого ссылаются объекты .net. использования сборщика мусора. Я вполне доволен GC, мой проблема не в GC.
  • Используйте финализаторы объектов. Финализаторы не детерминированным, в этом случае я необходимо детерминированное прекращение (например, использование /IDispose в .net)
  • Внедрение IUnknown в неуправляемом С++
    Если мне нужно перейти на С++-маршрут, я буду использовать ATL, спасибо.
  • Решите это с помощью Vb6 или повторно используйте Объекты VB6. Весь смысл это упражнение - удалить нашу сборку зависимость от Vb6.

Спасибо
BW

Принятый ответ
Люди из тысячи благодаря Steve Steiner, которые придумали единственный (возможно, работоспособный) ответ на основе .net и Earwicker, который придумал очень простое решение ATL.

Однако принятый ответ идет на Bigtoe, который предлагает обернуть объекты .net в объектах VbScript (которые я не считал честными), эффективно предоставляя простое решение VbScript для проблемы с VbScript.

Спасибо всем.

4b9b3361

Ответ 1

OK, это еще одна попытка. Фактически вы можете использовать "Windows Script Components", чтобы обернуть ваши COM-объекты .NET и таким образом получить финализацию. Здесь полный образец с использованием простого .NET Calculator, который может добавлять значения. Я уверен, что вы получите концепцию оттуда, это полностью исключает проблемы VB-Runtime, ATL и использует хост Windows Scripting Host, который доступен на всех основных платформах WIN32/WIN64.

Я создал простой класс .NET.NET под названием Calculator в пространствах имен, называемых DemoLib. Обратите внимание, что это реализует IDisposable, где для демонстрационной цели я помещаю что-то на экран, чтобы показать, что оно завершено. Я полностью придерживаюсь vb здесь, в .NET и Script, чтобы все было просто, но часть .NET может быть в С# и т.д. Когда вы сохраняете этот файл, вам нужно зарегистрировать его с помощью regsvr32, ему нужно будет сохраняться как нечто вроде 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

Далее я создаю компонент Windows Script, называемый Calculator.Lib, который имеет один метод, который возвращает обратно класс VB- Script COM, который предоставляет библиотеку .NET Math. Здесь я вывожу что-то на экране во время "Строительство и уничтожение", обратите внимание, что в Destruction мы вызываем метод Dispose в библиотеке .NET, чтобы освободить ресурсы там. Обратите внимание на использование функции Lib() для возврата .NET Com Calculator вызывающему.

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

Наконец, чтобы связать все это здесь, образец VB Script, где вы получаете диалоги, показывающие создание, вычисление, распоряжение, вызываемое в библиотеке .NET, и, наконец, завершение в COM-компоненте, представляющем .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.")

Ответ 2

Я понимаю, что это несколько старый вопрос, но я действительно получил фактический запрос на работу некоторое время назад.

Что это значит - заменить Release в VTBL (s) созданного объекта с помощью специальной реализации, которая вызывает Dispose, когда все ссылки были выпущены. Обратите внимание: нет никаких гарантий, что это всегда будет работать. Основное предположение заключается в том, что все методы Release на всех интерфейсах стандартного CCW являются одним и тем же методом.

Используйте на свой страх и риск.:)

/// <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 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 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);
    }
}

Ответ 3

Я не проверил это, но вот что я хотел бы попробовать:

Во-первых, здесь статья блога CBrumme о внедрении IMarsh по умолчанию для clr. Если ваши утилиты используются в квартирах COM, вы не получите надлежащего поведения от прямого порта VB6 до CLR. Объекты Com, реализованные CLR, действуют так, как если бы они объединили свободную резьбовую маршаллеру, а не модель с резьбой, которая была выставлена ​​VB6.

Вы можете реализовать IMarshal (в классе clr, который вы представляете как объект com). Я понимаю, что вы сможете контролировать создание прокси-сервера COM (а не прокси-сервера interop). Я думаю, что это позволит вам захватить вызовы Release в объекте, который вы вернули из UnmarshalInterface, и вернуть его обратно к исходному объекту. Я бы обернул стандартный маршаллер (например, pinvoke CoGetStandardMarshaler) и переадресует все вызовы на него. Я считаю, что объект будет иметь всю жизнь, привязанную к времени жизни CCW.

снова... вот что я попробую, если бы мне пришлось его решить на С#.

С другой стороны, будет ли такое решение действительно проще, чем внедрение в ATL? Просто потому, что магическая часть написана на С#, не делает решение простым. Если то, что я предлагаю выше, решает проблему, вам нужно написать действительно большой комментарий, объясняющий, что происходит.

Ответ 4

Я тоже борется с этим, чтобы попытаться добиться правильного срока службы сервера для моего обработчика Preview, как описано здесь: Просмотр данных с помощью нашей управляемой структуры обработчика предварительного просмотра

Мне нужно было запустить его на сервер обработки, и у меня были проблемы с управлением временем.

Способ доступа к серверу без обработки описан здесь для всех, кто интересуется: Содержимое сообщества RegistrationSrvices.RegisterTypeForComClients что подразумевает, что вы можете это сделать, внедряя IDispose, но это не сработало.

Я попытался реализовать финализатор, который в конечном итоге заставил объект быть выпущенным, но из-за шаблона использования сервера, вызывающего мой объект, это означало, что мой сервер повесился навсегда. Я также попробовал открутить рабочий элемент и после сна, заставляя мусор собирать, но это было действительно грязно.

Вместо этого он перешел к подключению Release (и AddRef, потому что нельзя было доверять возвращаемому значению Release).

(Найдено через эту почту: http://blogs.msdn.com/b/oldnewthing/archive/2007/04/24/2252261.aspx#2269675)

Вот что я сделал в моем конструкторе объектов:

//  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; 
}

Ответ 5

Структура

.Net работает по-разному: .NET Framework предоставляет методы управления памятью, которые отличаются от способа управления памятью в мире на основе COM. Управление памятью в COM было путем подсчета ссылок..NET обеспечивает автоматическую технологию управления памятью, которая включает отслеживание ссылок. В этой статье мы рассмотрим технику сбора мусора, используемую CLR Common Language Runtime CLS.

ничего не нужно делать

[EDITED] более одного раунда...

Взгляните на эту альтернативу Импорт библиотеки типов в виде сборки
Как вы сами сказали, используя CCW, вы можете получить доступ к ссылочному счету в традиционном модуле COM.

[EDITED] Настойчивость - это добродетель Вы знаете WinAPIOverride32? С его помощью вы можете захватывать и изучать, как это работает. Другим инструментом, который может помочь, является Deviare COM Spy Console.
Это будет нелегко.
Удачи.

Ответ 6

Насколько мне известно, лучший охват этой темы содержится в книге Руководство по интероперабельности .NET и COM Автор Alan Гордон, и эта ссылка должна перейти на соответствующую страницу в Google Книгах. (К сожалению, у меня его нет, я пошел вместо Troelsen book)

Указание там означает, что не существует четко определенного способа подключения к подсчету Release/reference в CCW. Вместо этого следует предположить, что вы делаете свой класс С# одноразовым и призываете своих COM-клиентов (в вашем случае авторов VBScript) называть Dispose, когда они хотят, чтобы детерминированная финализация произошла.

Но, к счастью, для вас есть лазейка, потому что ваши клиенты являются поздними связующими COM-клиентами, потому что VBScript использует IDispatch для всех вызовов объектов.

Предположим, что ваши классы С# были открыты через COM. Сначала выполните эту работу.

Теперь в ATL/С++ создайте класс-оболочку, используя мастер ATL Simple Object, а на странице параметров выберите Interface: Custom вместо Dual. Это останавливает работу мастера в своей поддержке IDispatch.

В конструкторе класса используйте CoCreateInstance для создания экземпляра вашего класса С#. Запросите его для IDispatch и удерживайте указатель в элементе.

Добавьте IDispatch в список наследования класса оболочки и переместите все четыре метода IDispatch прямо к указателю, который вы отложили в конструкторе.

В FinalRelease обертки используйте метод позднего связывания (Invoke), чтобы вызвать метод Dispose объекта С#, как описано в книге Алана Гордона (на страницах, которые я ссылался выше).

Итак, теперь ваши клиенты VBScript разговаривают через CCW с классом С#, но вы можете перехватить окончательный выпуск и переслать его методу Dispose.

Сделайте свою библиотеку ATL открытой отдельной оболочкой для каждого "реального" класса С#. Вероятно, вы захотите использовать наследование или шаблоны для повторного использования кода. Каждому классу С#, который вы поддерживаете, требуется только пара строк в коде обтекания ATL.

Ответ 7

Я думаю, причина в том, что это невозможно: refcount из 0 не означает, что объект не используется, потому что у вас может быть граф вызовов, например

VB_Object
   |
   V
   |
Managed1 -<- Managed2

и в этом случае объект Managed1 все еще используется, даже если объект VB отбрасывает ссылку на него, поэтому его refcount равен 0.

Если вам действительно нужно делать то, что вы говорите, я думаю, вы могли бы создать классы-оболочки в неуправляемом С++, который вызывает метод Dispose, когда refcount опустится до 0. Эти классы, вероятно, были бы связаны с метаданными, но у меня есть нет никакого опыта в том, как это реализовать.

Ответ 8

Из .NET запросите объект IUnknown на объекте. Вызовите AddRef(), затем Release(). Затем возьмите возвращаемое значение AddRef() и запустите с ним.

Ответ 9

Почему бы не сдвинуть парадигму. Как создать собственный агрегат вокруг открытого и расширенного с помощью методов уведомления. Это даже можно сделать в .Net не только ATL.

EDITED: Вот некоторая ссылка, которая может быть описана другим способом (http://msdn.microsoft.com/en-us/library/aa719740(VS.71).aspx). Но следующие шаги объясняют мою идею выше.

Создайте новый класс .Net, который реализует ваш устаревший интерфейс (ILegacy) и новый интерфейс (ISendNotify) с помощью одного метода:

interface ISendNotify 
{
     void SetOnDestroy(IMyListener );
}

class MyWrapper : ILegacy, ISendNotify, IDisposable{ ...

Внутри MyClass создайте экземпляр вашего реального унаследованного объекта и делегируйте все вызовы из MyClass в этот экземпляр. Это агрегация. Таким образом, время жизни агрегата зависит от MyClass. Поскольку MyClass является IDisposable, теперь вы можете перехватывать, когда экземпляр удален, поэтому вы можете отправлять уведомления с помощью IMyListener

EDIT2: Взято там (http://vb.mvps.org/hardcore/html/countingreferences.htm) простейший смысл IUnknown при отправке события

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

Ответ 10

Насколько мне известно, GC уже обеспечивает поддержку того, что вы пытаетесь сделать. Он называется Finalization. В чисто управляемом мире лучшей практикой является избегать Finalization, так как она имеет некоторые побочные эффекты, которые могут негативно повлиять на производительность и работу GC. Интерфейс IDisposable обеспечивает чистый, управляемый способ обхода завершения объекта и обеспечения очистки как управляемых, так и неуправляемых ресурсов из управляемого кода.

В вашем случае вам нужно инициировать очистку управляемого ресурса после того, как все неуправляемые ссылки будут выпущены. Финализация должна преуспеть в решении вашей проблемы здесь. GC будет завершать объект, всегда, если присутствует финализатор, независимо от того, как были выпущены последние ссылки на завершаемый объект. Если вы реализуете финализатор в своем .NET-типе (просто реализуете деструктор), тогда GC поместит его в очередь финализации. Как только цикл сбора GC будет завершен, он обработает очередь финализации. Любая работа по очистке, которую вы выполняете в своем деструкторе, будет возникать после обработки очереди финализации.

Следует отметить, что если ваш финалируемый тип .NET содержит ссылки на другие объекты .NET, которые, в свою очередь, требуют завершения, вы можете вызвать длинную коллекцию GC, или некоторые из объектов могут выживать дольше, чем без завершения ( что означало бы, что они выживут в коллекции и достигнут следующего поколения, которое собирается менее часто.) Однако, если работа по очистке ваших объектов .NET, использующих CCW, не чувствительна ко времени, и использование памяти не является большой проблемой, некоторое дополнительное время жизни не должно иметь значения. Следует отметить, что конечные объекты должны создаваться с осторожностью, а минимизация или устранение ссылок на уровень экземпляра класса других объектов может улучшить общее управление памятью через GC.

Подробнее о финализации вы можете прочитать в этой статье: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx. Хотя это довольно старая статья, когда .NET 1.0 был впервые выпущен, фундаментальная архитектура GC пока не изменилась (первые значительные изменения в GC будут поступать с .NET 4.0, однако они больше связаны с одновременное выполнение GC без замораживания потоков приложений, чем изменения его основной операции.)