Можно ли перехватить (или знать о) COM-подсчет ссылок на объекты CLR, выставленные COM - PullRequest
17 голосов
/ 08 февраля 2010

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

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

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

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

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

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

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

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

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

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

Спасибо
BW

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

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

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

Ответы [ 10 ]

5 голосов
/ 02 августа 2011

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

Что он делает, это заменяет 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'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 голосов
/ 12 февраля 2010

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

Я создал простой класс COM .NET с именем Calculator в пространствах имен с именем DemoLib. Обратите внимание, что это реализует IDisposable, где для демонстрации я помещаю что-то на экран, чтобы показать, что оно прекратилось. Я полностью придерживаюсь vb здесь, в .NET и скрипте, чтобы все было просто, но часть .NET может быть в C # и т. Д. Когда вы сохраните этот файл, вам нужно будет зарегистрировать его в 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 под названием Calculator.Lib, который имеет единственный метод, который возвращает обратно класс VB-Script COM, который предоставляет библиотеку .NET Math. Здесь я выскакиваю что-то на экран во время строительства и разрушения, обратите внимание, что в разделе «Разрушение» мы вызываем метод 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-скрипта, где вы получаете диалоги, показывающие создание, вычисление, избавление от вызова в библиотеке .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.")
4 голосов
/ 14 февраля 2010

Я не проверял это, но вот что я попробую:

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

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

снова ... это то, что я бы попробовал, если бы мне пришлось решать это в C #.

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

3 голосов
/ 24 мая 2011

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

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

Способ получения доступа к серверу вне процесса описан здесь для всех, кто интересуется: Содержимое сообщества 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; 
}
2 голосов
/ 15 февраля 2010

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

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

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

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

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

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

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

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

Итак, теперь ваши клиенты VBScript общаются через CCW с классом C #, но вы можете перехватить финальную версию и перенаправить ее в метод Dispose.

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

2 голосов
/ 12 февраля 2010

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

ничего не поделаешь

[отредактировано] еще один раунд ...

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

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

0 голосов
/ 16 февраля 2010

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

0 голосов
/ 14 февраля 2010

Насколько мне известно, 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 без замораживания потоков приложения, чем с изменениями в его основной работе.)

0 голосов
/ 09 февраля 2010

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

VB_Object
   |
   V
   |
Managed1 -<- Managed2

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

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

0 голосов
/ 08 февраля 2010

Почему бы не сменить парадигму. Как насчет создания вашей собственной совокупности вокруг открытых и расширенных с помощью методов уведомлений. Это может быть сделано в .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
...