Можно ли перехватывать (или быть в курсе) 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.
Спасибо всем.
Ответы
Ответ 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 без замораживания потоков приложений, чем изменения его основной операции.)