【问题标题】:Hosting managed code and garbage collection托管托管代码和垃圾收集
【发布时间】:2014-04-09 06:27:10
【问题描述】:

我有一个 C++ 进程外 COM 服务器,它承载大量 C# 代码以支持 C++ COM 对象公开的 API。

出于多种原因,我正在考虑取消我的解决方案中的 C++ 部分。但是,由于我无法控制的限制,我必须保留进程外 COM 服务器。微软确实有这个here 的典型示例。

看这个例子有一些我不明白的地方。在消息循环开始之前,会创建一个计时器以每 5 秒调用一次 GC.Collect。我能找到的唯一提及这一点表明这是为了确保在合理的时间范围内释放 COM 对象。我对此有点困惑……我的 C++ 主机当前是否自动调用 GC.Collect?我当然不会这样做。然而我正在创建托管对象(使用 COMVisible(true) 作为 C++ 代码中的 COM 对象。这是否意味着我现在应该每 5 秒调用一次 GC.Collect?如果不是,为什么我需要在这个新的 C# 中调用它进程外服务器。这是为了弥补在普通 C++ 应用程序中清理未引用 COM 对象的自动进程吗?(我假设在消息循环期间的某个时间发生。)

每 5 秒调用一次 GC.Collect 似乎是个坏主意。我担心错了吗?有没有其他方法可以达到同样的效果?

我正在使用 .NET 4.5 和 Visual Studio 2012。

【问题讨论】:

    标签: c# c++ garbage-collection com-interop


    【解决方案1】:

    IMO,在 C# 中创建 COM 进程外服务器的最简单方法是使用 DLL surrogate process

    您仍然受限于双接口 (ComInterfaceType.InterfaceIsDual),并且您需要注册生成的类型库(并将其作为部署的一部分):

    C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe ManagedServer.dll /codebase /tlb

    这将允许您利用 COM 类型库编组器,因为您没有用于 C# COM 对象的专用 COM 代理/stuf DLL。

    确保使用正确的RegAsm.exe 二进制文件,具体取决于ManagedServer.dll 程序集的目标位数。以上假设 x86 代码。

    这是一个完整的工作模板示例。它负责代理注册:

    using Microsoft.Win32;
    using System;
    using System.Runtime.InteropServices;
    
    namespace ManagedServer
    {
        [ComVisible(true), Guid("1891CF89-1282-4CA8-B7C5-F2608AF1E2F1")]
        [InterfaceType(ComInterfaceType.InterfaceIsDual)]
        public interface IManagedComObject
        {
            string ComMethod(string data);
        }
    
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        [ComDefaultInterface(typeof(IManagedComObject))]
        [Guid("989162CD-A6A6-4A7D-A7FB-C94086A4E90A")]
        [ProgId("Noseratio.ManagedComObject")]
    
        public class ManagedComObject : IManagedComObject
        {
            // public constructor
            public ManagedComObject()
            {
            }
    
            // IManagedComObject
            public string ComMethod(string data)
            {
                return data;
            }
    
            // registration
            [ComRegisterFunction()]
            public static void Register(Type type)
            {
                var guid = type.GUID.ToString("B");
                using (var appIdKey = Registry.ClassesRoot.CreateSubKey(@"AppID\" + guid))
                {
                    appIdKey.SetValue("DllSurrogate", String.Empty);
                }
                using (var appIdKey = Registry.ClassesRoot.CreateSubKey(@"CLSID\" + guid))
                {
                    appIdKey.SetValue("AppId", guid);
                }
            }
    
            [ComUnregisterFunction()]
            public static void Unregister(Type type)
            {
                var guid = type.GUID.ToString("B");
                using (var appIdKey = Registry.ClassesRoot.OpenSubKey(@"AppID\" + guid, writable: true))
                {
                    if (appIdKey != null)
                        appIdKey.DeleteValue("DllSurrogate", throwOnMissingValue: false);
                }
                Registry.ClassesRoot.DeleteSubKeyTree(@"CLSID\" + guid, throwOnMissingSubKey: false);
            }
        }
    }
    

    默认情况下,对象将在 MTA 单元中创建,因此接口方法可能会在任何线程上调用,您需要实现线程安全。

    如果您需要在对象的代理进程内使用带有消息泵的 STA 线程,您可以通过实现工厂单例并使用 CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream 导出 STA 线程外部的对象 (this可能是相关的)。

    另外一点,在创建ManagedComObject 的实例时,您的COM 客户端代码应该使用CLSCTX_LOCAL_SERVERActivator.CreateInstance(Type.GetTypeFromProgID("Noseratio.ManagedComObject")) 不是这种情况,它显然使用了CLSCTX_ALL。这很容易解决:

    using System;
    using System.Runtime.InteropServices;
    
    namespace Client
    {
        class Program
        {
            static void Main(string[] args)
            {
                // dynamic obj = Activator.CreateInstance(Type.GetTypeFromProgID("Noseratio.ManagedComObject"));
    
                dynamic obj = ComExt.CreateInstance(
                    Type.GetTypeFromProgID("Noseratio.ManagedComObject").GUID, 
                    localServer: true);
    
                Console.WriteLine(obj.ComMethod("hello"));
            }
        }
    
        // COM interop
        public static class ComExt
        {
            const uint CLSCTX_LOCAL_SERVER = 0x4;
            const uint CLSCTX_INPROC_SERVER = 0x1;
    
            static readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");
    
            [DllImport("ole32.dll", ExactSpelling = true, PreserveSig = false)]
            static extern void CoCreateInstance(
               [MarshalAs(UnmanagedType.LPStruct)] Guid rclsid,
               [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter,
               uint dwClsContext,
               [MarshalAs(UnmanagedType.LPStruct)] Guid riid,
               [MarshalAs(UnmanagedType.Interface)] out object rReturnedComObject);
    
            public static object CreateInstance(Guid clsid, bool localServer)
            {
                object unk;
                CoCreateInstance(clsid, null, localServer ? CLSCTX_LOCAL_SERVER : CLSCTX_INPROC_SERVER, IID_IUnknown, out unk);
                return unk;
            }
        }
    }
    

    【讨论】:

    • 这是一个很好的答案,有很多很好的信息。我在其他地方看到过类似的答案,对于一个新对象,这会很好用。但是,我不确定它是否能满足我的需求。我对创建自己的消息泵有点担心...我已经做过很多次了,并且在您引用的答案中使用了 MsgWaitForMultipleObjectsEx。但是 dllhost.exe 不是必须有自己的消息循环吗?此外,许多其他进程使用 dllhost。如果客户崩溃,我们的客户支持可能很难找到并终止他们偶尔必须做的任务。我可能仍然认为这是答案
    • @Griffin,我不认为DllHost.exe 代理进程在这种情况下会创建消息循环。默认情况下,.NET COM 对象在注册表中用Both 线程模型标记。我确定DllHost 在这种情况下使用了一个 MTA 线程池,没有消息循环(我还没有验证这一点)。您可以使用自己的代理进程而不是使用DllHost,但您仍然需要将托管COM 服务器塑造为DLL(对于RegAsm/TLB)。除非您想使用 MIDL 手动构建 TLB 或使用现有的用于 IPC 的 COM 编组器感知接口,例如 IDispatchIOleCommandTarget
    猜你喜欢
    • 1970-01-01
    • 2010-10-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-08
    • 1970-01-01
    • 1970-01-01
    • 2017-05-07
    相关资源
    最近更新 更多