Marshal.ReleaseComObject并返回值

时间:2013-06-28 20:53:26

标签: .net com-interop

我正在寻找管理.Net程序集中创建的COM对象生命周期的想法,然后将这些想法传递回非托管代码。

背景: 我们的软件引擎采用非托管c ++编码。我们的软件功能可以使用引擎的COM对象组件进行扩展。在大多数情况下,发动机本身不需要多年改变。但是,我们继续构建更多组件以向我们的软件添加新功能。虽然这些组件过去也是用非托管c ++构建的,但在过去的几年里,我们一直用C#编写这些插件。

直到最近,我们已经订阅了Marshal.ReleaseComObject的想法,不应该在C#组件中使用的非托管COM对象上调用它,而是允许RCW和垃圾回收管理它们的生命周期。

这个理论听起来不错,但实际上我们已经开始对我们的软件存在内存问题,这似乎是因为GC在清理这些COM对象方面很懒,以至于在某些情况下我们的软件会耗尽所有可用的内存并失败。

我们可能会犯一些错误,阻止GC和RCW对此对象采取行动吗?不是垃圾收集的想法,它会做什么是必要的,以确保在需要时内存是免费的?

由于缺乏任何更好的想法,我们不情愿地开始使用Marshal.ReleaseComObject(在某些情况下还有FinalReleaseComObject)。一般来说,这似乎可以解决问题。

然而,在尝试使用Marshal.ReleaseComObject开发一致模式时,我们发现了一个我们不确定是否可以使用它的情况。某些组件需要将COM对象返回值传递回非托管引擎。组件将永远不会再次看到返回值,并且(当前)在不再使用时无法得到通知。例如:

// Unmanaged C++ code
Engine::Engine() : m_ipDoodadCreator(CLSID_FancyComponent)
{
}
void Engine::ProcessDoodad()
{
    try
    {
        // Smart pointer
        IDoodadPtr ipDoodad = m_ipDoodadCreator->CreateDoodad();
        ...
        ipDoodad = NULL; // Calls Release...
    }
    catch (...) { ... }
    // At this point the doodad lives on because the RCW is holding a
    // reference to it.
}

// Managed C# Component
public class FancyComponent : IDoodadCreator
{
    public IDoodad IDoodadCreator.CreateDoodad()
    {
        // IDoodad is implmented in unmanaged c++
        IDoodad doodad = new IDoodad();

        try
        {
            ...

            return doodad;
        }
        finally
        {
            // Can't do this here because engine doesn't yet have
            // a reference to doodad.
            // Marshal.ReleaseComObject(doodad);
        }
    }
}

到目前为止,为了确定性地让RCW在这种情况下释放它的引用,我们可以提出的唯一想法是重新启动引擎,以便所有接口都有一个调用的Cleanup()调用不时,但这将是耗时的,在某些情况下,实施起来非常麻烦。

tl; dr 有没有办法强制RCW释放对返回到非托管环境的COM对象的引用?

3 个答案:

答案 0 :(得分:1)

  

但实际上我们已经开始使用我们的软件存在内存问题

这是一个经典的成长痛苦问题。软件永远不会变小,团队中没有人因编写否定代码而获奖。添加越来越多的功能,您的程序永远不会使用 less 内存。标准诊断是消耗第一千兆字节的虚拟内存空间需要很长时间。第二个千兆字节很快消失了。

你现在所处的非常非生产性,你正在担心少数几兆字节。只需将开关拨到非问题状态即可。将本机代码编译为64位。您不会受到托管代码的任何异议,x64抖动已经知道如何在没有任何更改的情况下执行此操作。

答案 1 :(得分:1)

我已经确定了两种可能的解决方案。两者都涉及更改 COM 接口的托管定义,尽管您根本不需要更改非托管代码。

  1. 将您的返回值定义为 IntPtr。然后,当您的托管方法准备好返回时,通过 IntPtr 从您的 COM 对象获取一个 Marshal.GetIUnknownForObject()。此时,您可以安全地对 COM 对象调用“Marshal.ReleaseComObject()”。
    public interface IDoodadCreator
    {
        IntPtr IDoodadCreator.CreateDoodad();
    }

    public class FancyComponent : IDoodadCreator
    {
        public IntPtr IDoodadCreator.CreateDoodad()
        {
            // IDoodad is implemented in unmanaged c++
            IDoodad doodad = new IDoodad();
            ...
    
            IntPtr doodadPtr = Marshal.GetIUnknownForObject(doodad);
            Marshal.ReleaseComObject(doodad);
            return doodadPtr;
        }
    }

  1. 将返回值定义为 object,并定义接口以使用自定义封送拆收器,在将对象封送至 IntPtr 后释放该对象。
    public interface IDoodadCreator
    {
        [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ReleasingMarshaler))]
        object IDoodadCreator.CreateDoodad();
    }

    public class FancyComponent : IDoodadCreator
    {
        public object IDoodadCreator.CreateDoodad()
        {
            // IDoodad is implemented in unmanaged c++
            IDoodad doodad = new IDoodad();
            ...
    
            return doodad;
        }
    }

    public class ReleasingMarshaler : ICustomMarshaler
    {
        ...

        public IntPtr MarshalManagedToNative(object managedObj)
        {
            IntPtr managedObjPtr = Marshal.GetIUnknownForObject(managedObj);
            Marshal.ReleaseComObject(managedObj);
            return managedObjPtr;
        }
    }

答案 2 :(得分:-1)

我们当前问题的答案是GC.AddMemoryPressure。

因为我们是通过未管理类的RCW实例创建和引用的,所以垃圾收集器不知道为这些对象分配的内存。 根据MSDN对此方法的帮助:

  

在确定何时计划垃圾收集时,运行时会考虑分配的管理内存量。如果小型托管对象分配大量非托管内存,则运行时仅考虑托管内存,因此低估了调度垃圾回收的紧迫性。

http://msdn.microsoft.com/en-us/library/system.gc.addmemorypressure.aspx

换句话说,并不是我们需要显式释放COM对象......我们只需要告诉GC有多少内存与它们相关联。实际上,在某些关键位置实现此方法后,垃圾收集器的表现要好得多。