【问题标题】:Release COM interface without the knowledge of coclass在不知道 coclass 的情况下释放 COM 接口
【发布时间】:2012-07-17 15:27:56
【问题描述】:

我有一个 C# 客户端,它使用来自本机 C++ COM 服务器 dll 的接口。 DLL 实现了 4 个接口。这 4 个接口由 DLL 中的 4 个组件类实现。但是只有 1 个 coclass 暴露给客户。接口 2,3,4 通过接口 1 中的方法之一返回给客户端。

C++ COM 服务器:

interface IFace1: IUnknown{
HRESULT CreateOtherInterface([in] REFIID iidFace, [out, iid_is(iidFace)] void** ppOut);
};

coclass ClassIFace1
{
    [default] interface IFace1;
};

C# 客户端:

ClassIFace1 Face1Obj = new ClassIFace1();

IFace1 Face1Ctrl = (IFace1)Face1Obj; 

IFace2 Face2Ctrl = null;
IntPtr Face2IntPtr = new IntPtr();

Face1Ctrl.CreateOtherInterface(Face2Guid, out Face2IntPtr);
Face2Ctrl = (IFace2)Mashal.PtrToStructure(Face2IntPtr);

//Consume Face2Ctrl

if(Face1Obj != null)
{
    Marshal.ReleaseComObject(Face1Obj);
}

由于 IFace2、IFace3 和 IFace4 与 IFace1 不共享相同的 coclass,我怀疑 Marshal.ReleaseComObject(Face1Obj) 行只会破坏 ClassIFace1 对象,但不会破坏 ClassIFace2、ClassIFace3、ClassIFace4 对象并导致内存泄漏。有没有办法解决这个问题?还是 Marshal.ReleaseComObject(Face1Obj) 实际上也破坏了其他 COM 对象?

【问题讨论】:

  • 也许我应该在这里更具体一些。 COM 服务器将与设备通信。并且不同的设备暴露了一组不同的功能。每个特性都实现为一个特性接口,如IFace2、IFace3等。客户端只需要给出他们感兴趣的特性的iid,如果设备支持,COM服务器将返回该特性接口,否则调用失败.
  • 客户不知道 IFace2、IFace3 等是否使用与 IFace1 相同的 coclass 实现(实际上它们是不同的 coclass),他们也不关心。他们只是从 IFace1 中获取所有功能接口,并在完成后单独发布功能接口。这种方法多年来一直适用于 C++ 客户端,直到我们需要实现一个 C# 客户端来与 COM 服务器对话。

标签: c# com interface interop


【解决方案1】:

就像汉斯所说的那样,CreateOtherInterface 看起来很奇怪。通常,您不需要自己创建它。您需要做的就是确保客户端可以访问所有四个 coclass。然后,Activator.CreateInstance 或本机 CoCreateInstance 将为您做正确的事情。另一种选择是公开一个单独的 coclass 并让该单独的 coclass 支持所有四个接口。

但是,由于您提到只有 1 个 coclass 向客户端公开,我想有一些奇怪的原因是客户端使用的 TLB 文件看不到其他 3 个 coclass 或其他 3 个 coclass 未正确注册但由第一个同班同学以某些专有方式发现。我还假设您不能修改服务器端实现。

鉴于所有这些假设,这是我的答案。引用计数在 4 个 coclass 中独立维护。因此,释放第一个 coclass 的引用不会减少其他三个 coclass 的引用计数。

还有一些你需要注意的事情。您正在使用 Marshal.ReleaseComObject(Face1Obj) 发布第一个 coclass。您可以这样做,因为第一个 coclass 是由 Runtime Callable Wrapper (RCW) 包装的。正如 Martin 所说,即使您不调用 Marshal.ReleaseComObject(),.NET 运行时也会在发生垃圾收集时为您执行此操作。

但是,Face2Ctrl 的获取方式不同。它没有被 RCW 包裹。您将返回的指针直接视为结构。这对我来说听起来不对,因为您可能在内存对齐和数据编组方面遇到问题。您想要做的可能是调用Marshal.GetObjectForIUnknown,它将为您返回一个RCW。拿到RCW后,可以拨打Marshal.ReleaseComObject()及时释放RCW。

如果CreateOtherInterface 的实现类似于QueryInterface,在返回的接口上总是AddRef,则在完成Face2Obj 后,您应该在返回的接口上调用Marshal.ReleaseMarshal.ReleaseComObject() 是不够的,因为它只是释放了 RCW 添加的引用计数,但在这种情况下,您需要对 IUnknown.Release 进行多次调用

【讨论】:

  • 对于 coclass,当我有 40+ 个接口时,要求客户端访问所有 40+ 个 coclass 确实不太好。此外,COM 服务器需要做一些初始化代码并将一些一般信息传递给所有 40+ 个 coclass,目前在 coclassFace1 中完成,当调用 CreateOtherInterface() 时,coclassFace1 将创建其他 coclass 并将初始化信息传递给类在将新接口指针返回给客户端之前。
  • 如果我理解正确,您是在建议我的客户将 IFace2 接口转换为 IntPtr 然后使用Marshal.GetObjectForIUnknownMarshal.ReleaseComObject(Face2Obj) 发布。这个对吗?然后用Marshal.Release()释放IFace2的IntPtr?但是Face2 COM 对象不应该已经被Marshal.ReleaseComObject(Face2Obj) 完全释放了吗?除非Marshal.ReleaseComObject(Face2Obj) 只释放RCW 而不是底层的原生COM 对象......?但是,这是一个很好的建议,我一定要试一试..谢谢..
【解决方案2】:

COM 对象的正常行为是通过它们的接口独占访问它们。 coclass 仅在创建 COM 对象的新实例时是必需的,您不能直接访问 coclass。所以我怀疑你的例子应该是这样的:

IFace1 face1Ctrl = new ClassIFace1();

CreateOtherInterface() 方法对我来说有点奇怪,它与 QueryInterface() 具有相同的签名,所以我认为它应该做同样的事情(我不熟悉 C++):

IFace2 face2Ctrl;
face1Ctrl.CreateOtherInterface(IFace2, out face2Ctrl);

我觉得应该可以,试试吧。如果是普通的 QueryInterface 方法,你应该可以得到这样的接口:

IFace2 face2Ctrl = face1Ctrl as IFace2;

COM 对象是引用计数的,一旦您销毁对接口的最后一个引用,它们就会被释放。一旦垃圾收集器使用对 COM 对象的引用销毁您的变量,COM 对象就会自行释放。这在 C# 中可能是一个问题,因为您必须等待垃圾收集器并且无法确定释放的顺序。如果您需要在给定时刻释放 COM 对象,您可以使用Marshal.ReleaseComObject(),但通常您只需等待垃圾收集器递减引用计数器。

只要你不知道 COM 对象的实现,你就不知道每个接口是否有它自己的 coclass,或者一个 coclass 是否实现了多个接口。当您使用 COM 对象时,您应该不需要这些知识。查询接口可以创建一个新的 coclass 并返回它的接口,或者它可以返回自己并增加引用计数器。

【讨论】:

  • 是的,CreateOtherInterface() 的工作方式与 QueryInterface() 完全相同:传入接口 iid,它将返回接口指针。不同之处在于 QueryInterface() 仅返回在同一 coclass 上实现的接口(除非您自己覆盖 QueryInterface()),但 CreateOtherInterface() 可以返回任何接口,无论它们是否在同一 coclass 上实现。我不太明白您评论的最后一部分,您是否建议我应该等待垃圾收集器减少 CoclassFace2、CoclassFace3 的引用计数?
  • @Angela Yan - 是的,你可以等待垃圾收集器。当您调用CreateOtherInterface() 时,在您的情况下,它将创建一个类ClassIFace2 的新实例,并将向该对象返回一个接口IFace2。使用此接口,您可以增加对象的引用计数器。现在,您通过 IFace1 持有对第一个对象的引用,并通过 IFace2 持有对第二个对象的另一个引用。垃圾收集器将变量收集到 IFace1 后立即释放第一个对象,将变量收集到 IFace2 时释放第二个对象。
  • @Angela Yan - 上面的评论假设您使用像IntPtrMarshal 类这样的花哨的东西,这应该不是必需的,而且我如果可能的话会避免它。
  • 感谢您提供的信息。但是如果我想确定性地终止 COM 服务器而不是等待垃圾收集器,有什么办法吗?目前 CreateOtherInterface() 的第二个参数是一个 IntPtr (用于返回接口),基于我在添加 COM 服务器的类型库作为对 C# 项目的引用后在 C# 项目的对象浏览器中看到的内容。
  • 如果你需要确定性的行为,你可以强制垃圾收集器完成它的工作,或者如果你需要一个给定的顺序,你可以调用Marshal.ReleaseComObject()甚至Marshal.FinalReleaseComObject()。如果你这样做,你必须确保这些变量以后不会在你的代码中使用。对于 intPtr:该方法看起来很吓人,因为它不知道您将请求什么接口,但 DotNet 会注意您可以只传递所需接口的变量(参见第二个代码示例)。
【解决方案3】:

另外,在获取界面的方法上有一些错误。以下是 C# 客户端的完整解决方案:

//======Create IFace1 and IFace2 interface===============
Type consoleType = Type.GetTypeFromCLSID(Face1CoCLSID);
Object Face1Obj = Activator.CreateInstance(consoleType);
IFace1 Face1Ctrl = (IFace1)Face1Obj;

Guid IFace2Guid = typeof(IFace2).GUID;
IntPtr Face2IntPtr = IntPtr.Zero;

//Face2 object's ref count will go up 1
Face1Ctrl.CreateOtherInterface(Face2Guid, out Face2IntPtr); 

//Face2 object's ref count will go up 2. One by "GetObjectForIUnknown()" 
//and one by "as", since the "as" will trigger .Net to call QueryInterface()
IFace2 Face2Ctrl = Marshal.GetObjectForIUnknown(Face2IntPtr) as IFace2; 

//=============Consume Face2Ctrl=========================

//======Destroy IFace1 and IFace2 interface===============

if (Face2Ctrl != null)
{
//Release 3 times as there were 3 RefCount obtained.

    Marshal.Release(Face2IntPtr);
    Marshal.Release(Face2IntPtr);
    Marshal.Release(Face2IntPtr);
    Face2Ctrl = null;     
}

if(Face1Obj != null)
{
//both Face1 object and Face2 object will get FinalRelease() after
//this line.
    Marshal.ReleaseComObject(Face1Obj); 
    Face1Obj = null;
}

【讨论】:

    猜你喜欢
    • 2019-06-18
    • 1970-01-01
    • 1970-01-01
    • 2014-08-16
    • 1970-01-01
    • 2011-01-21
    • 1970-01-01
    • 2019-03-21
    • 1970-01-01
    相关资源
    最近更新 更多