在用 C# 编写的 COM+ 组件上处理 dispose

Handling dispose on a COM+ component written in C#

因此,此时我正在做的是将 vb6 class 重写为 C#。最终的结果是,它将被用作COM+组件。

假设我们有一个设置为 COM+ 组件的日志记录 class。从 vb6 开始,您可以这样使用它:

Set logger = CreateObject("LoggingComponent")

我用 C# 重写了它,安装了 C# class 作为 COM+ 组件,我可以在 vb6 中使用它。此时一切都很好。接下来是我的问题。

为了将日志写入文件,您必须调用执行实际写入的 Flush() 方法。到目前为止,消息都在内存中的队列中。我的问题是当我忘记调用 Flush 方法时会发生什么。在 vb6 中,它们被刷新了。在 C# 中,它们不会被刷新。区别来了:

原vb6代码中,有一个方法

Private Sub Class_Terminate()
    Flush()
End Sub

我假设这个可以确保即使我们不调用 flush,日志也会被写入。

在 C# 中,我实现了 IDisposable 和析构函数,但是当 vb6 应用程序完成并处理 COM+ 记录器实例时它们不会被调用(请忽略丢失的 {} 和其他无用的细节,我只是删除了它们使代码更易于阅读 https://msdn.microsoft.com/en-us/library/b1yfkh5e(v=vs.100).aspx):

public void Dispose()
     Flush(); // does not get called


~Logger()
     Flush(); // does not get called

所以...有人知道我错过了什么吗?为什么在这种情况下不会调用 ~Logger?是否有任何我可以处理的 COM+ 事件(如 Application.Current.Exit 事件)。

OP 的原始问题是 GC 不是 运行。一种选择是构建一个定期调用 GC.Collect 的计时器。这可能是最安全的解决方案。

我可以提供替代解决方案。此解决方案涉及大量黑客攻击,因此开发人员应提防此方法。我还没有彻底审查这个。使用 COM 进程外服务器(这里不使用 COM+)进行的简单测试证明这种方法基本有效,但我还没有花必要的时间来研究和测试它是否完全有效。随意进一步探索这个想法,但无需额外的研究和测试,我不建议你这样做

想法是用我们自己创建的方法换出底层IUnknown对象的vtable中的Release方法,这样我们就可以观察ref计数何时达到0。

// !!! THIS CODE INVOLVES A SERIOUS HACK !!!
// !!! USE AT YOUR OWN RISK              !!!

[ComVisible(true)]
[Guid(...)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IMyInterface))]
public class MyObject : IMyInterface, IDisposable
{
    // constructor
    public MyObject()
    {
        // get and store this object's IUnknown* (this adds a reference)
        _pUnknown = Marshal.GetIUnknownForObject(this);

        // get a pointer to the vtable of the IUnknown
        _pVTable = Marshal.ReadIntPtr(_pUnknown);

        // get a pointer to the Release method from the vtable
        var pRelease = Marshal.ReadIntPtr(_pVTable, 2 * IntPtr.Size);

        // get and store a delegate to the original Release method
        _originalRelease = (ReleaseDelegate) Marshal.GetDelegateForFunctionPointer(pRelease, typeof(ReleaseDelegate));

        // set the entry for the Release method in the vtable to a pointer for the ReleaseOverride method
        var pReleaseOverride = Marshal.GetFunctionPointerForDelegate(OverriddenRelease);
        Marshal.WriteIntPtr(_pVTable, 2 * IntPtr.Size, pReleaseOverride);
    }

    // this method will be called when a COM client releases
    private static int ReleaseOverride(IntPtr pUnknown)
    {
        // get the object being released
        var o = (MyObject) Marshal.GetObjectForIUnknown(pUnknown);

        // call the original release method
        var refCount = o._originalRelease(pUnknown);

        // if the remaining reference count is 1, the only
        // outstanding reference is the reference acquired through
        // the Marshal.GetIUnknownForObject call in the constructor
        if (refCount == 1)
        {
            // call Dispose
            o.Dispose();

            // restore the original Release method
            var pRelease = Marshal.GetFunctionPointerForDelegate(o._originalRelease);
            Marshal.WriteIntPtr(o._pVTable, 2 * IntPtr.Size, pRelease);

            // release the reference we acquired in the constructor
            refCount = Marshal.Release(o._pUnknown);
        }

        // return the ref count
        return refCount;
    }

    // this method will now be called when all COM clients release
    public void Dispose()
    {
    }

    // the IUnknown pointer for this object
    private readonly IntPtr _pUnknown;

    // a pointer to the v-table of the IUnknown
    private readonly IntPtr _pVTable;

    // a delegate to the original Release method
    private readonly ReleaseDelegate _originalRelease;

    // a delegate to the ReleaseOverride method
    private static readonly ReleaseDelegate OverriddenRelease = ReleaseOverride;

    // the Release delegate type
    private delegate int ReleaseDelegate(IntPtr pUnknown);
}